mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 16:41:50 +00:00 
			
		
		
		
	Compare commits
	
		
			598 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c83ecf764d | ||
| 
						 | 
					a2485a18cb | ||
| 
						 | 
					8ef2ad17b5 | ||
| 
						 | 
					4579f78bf9 | ||
| 
						 | 
					1853407645 | ||
| 
						 | 
					cb5efc1c42 | ||
| 
						 | 
					d26c2b1a44 | ||
| 
						 | 
					8bda8e5393 | ||
| 
						 | 
					954b8a0cff | ||
| 
						 | 
					7c17e72db4 | ||
| 
						 | 
					d180aee57f | ||
| 
						 | 
					e3ffecefc0 | ||
| 
						 | 
					4c61cf153c | ||
| 
						 | 
					c78fb90e2f | ||
| 
						 | 
					321504cf29 | ||
| 
						 | 
					0f4a7bf1f5 | ||
| 
						 | 
					711e74a12b | ||
| 
						 | 
					aa8eb2c92a | ||
| 
						 | 
					b422a63b2a | ||
| 
						 | 
					ad5f2cd748 | ||
| 
						 | 
					efae363739 | ||
| 
						 | 
					2d79d21c50 | ||
| 
						 | 
					3b9d126322 | ||
| 
						 | 
					896654aaef | ||
| 
						 | 
					5fad38f65f | ||
| 
						 | 
					89f2ea5725 | ||
| 
						 | 
					a32ad33b4e | ||
| 
						 | 
					a328fff5a7 | ||
| 
						 | 
					233783c76c | ||
| 
						 | 
					39a18fb358 | ||
| 
						 | 
					460a144ca8 | ||
| 
						 | 
					23ead416d5 | ||
| 
						 | 
					1b5f11bbee | ||
| 
						 | 
					0da97289e6 | ||
| 
						 | 
					91f12a50cf | ||
| 
						 | 
					e92a9d1d9e | ||
| 
						 | 
					4eb51ab4d6 | ||
| 
						 | 
					f1a8d957f8 | ||
| 
						 | 
					9821a3442b | ||
| 
						 | 
					87842e097b | ||
| 
						 | 
					7dd40e2014 | ||
| 
						 | 
					a719998220 | ||
| 
						 | 
					09a6fdf1c7 | ||
| 
						 | 
					d2616cbdfc | ||
| 
						 | 
					faf1c8bee8 | ||
| 
						 | 
					f09aca4865 | ||
| 
						 | 
					cc52f37933 | ||
| 
						 | 
					e5051eefbc | ||
| 
						 | 
					9e5cd0da51 | ||
| 
						 | 
					4e120a291e | ||
| 
						 | 
					2790d72bff | ||
| 
						 | 
					e44f447d85 | ||
| 
						 | 
					4356581db0 | ||
| 
						 | 
					f87a701b28 | ||
| 
						 | 
					fa2eb46cd6 | ||
| 
						 | 
					f924e80f43 | ||
| 
						 | 
					6180ee8065 | ||
| 
						 | 
					1be106c0b5 | ||
| 
						 | 
					b0533db2eb | ||
| 
						 | 
					dba502c756 | ||
| 
						 | 
					d9cb64b893 | ||
| 
						 | 
					2d91e6b977 | ||
| 
						 | 
					4a6f1f150a | ||
| 
						 | 
					7f76f3726f | ||
| 
						 | 
					e2d97b6f36 | ||
| 
						 | 
					2a653642f5 | ||
| 
						 | 
					97eba1eecc | ||
| 
						 | 
					ff6bed54c6 | ||
| 
						 | 
					f9b0666adf | ||
| 
						 | 
					ca12b8aa56 | ||
| 
						 | 
					77508f7e44 | ||
| 
						 | 
					54de0ca0da | ||
| 
						 | 
					f364788c03 | ||
| 
						 | 
					00aaf84c37 | ||
| 
						 | 
					b01bc76dc5 | ||
| 
						 | 
					910f812737 | ||
| 
						 | 
					a4d024f43d | ||
| 
						 | 
					9937ad7fa0 | ||
| 
						 | 
					edcd88123d | ||
| 
						 | 
					ea1b5e19f0 | ||
| 
						 | 
					54337befc2 | ||
| 
						 | 
					140ef791aa | ||
| 
						 | 
					58350b6c99 | ||
| 
						 | 
					f186ff8b46 | ||
| 
						 | 
					03190611bb | ||
| 
						 | 
					37f322585e | ||
| 
						 | 
					9218e85bd6 | ||
| 
						 | 
					f923ba87c0 | ||
| 
						 | 
					fac49896df | ||
| 
						 | 
					56225701f9 | ||
| 
						 | 
					b5de43b225 | ||
| 
						 | 
					b955527f6c | ||
| 
						 | 
					94b28102f5 | ||
| 
						 | 
					de871862a8 | ||
| 
						 | 
					3be56fd502 | ||
| 
						 | 
					5086cd716f | ||
| 
						 | 
					4937af0cd9 | ||
| 
						 | 
					877a5fda41 | ||
| 
						 | 
					39cd2838df | ||
| 
						 | 
					565473c90c | ||
| 
						 | 
					1fac91a659 | ||
| 
						 | 
					0a4837c1f0 | ||
| 
						 | 
					e7404183a0 | ||
| 
						 | 
					44f8dcfb6e | ||
| 
						 | 
					481e0e98f8 | ||
| 
						 | 
					9de40c26eb | ||
| 
						 | 
					ad953f02d1 | ||
| 
						 | 
					3869e56521 | ||
| 
						 | 
					63d87b17aa | ||
| 
						 | 
					ed68a0e773 | ||
| 
						 | 
					e2640c8368 | ||
| 
						 | 
					eff626248f | ||
| 
						 | 
					ce29a3b07a | ||
| 
						 | 
					1b89174558 | ||
| 
						 | 
					1c1ad32610 | ||
| 
						 | 
					71237e2f76 | ||
| 
						 | 
					518c271eba | ||
| 
						 | 
					d71996e58d | ||
| 
						 | 
					2f33cd2db5 | ||
| 
						 | 
					5ec9bb0fb5 | ||
| 
						 | 
					8cc3cbb22e | ||
| 
						 | 
					b0fa317302 | ||
| 
						 | 
					5cb56bc677 | ||
| 
						 | 
					21f8fd9fa5 | ||
| 
						 | 
					2100ef63a9 | ||
| 
						 | 
					29db77c9c9 | ||
| 
						 | 
					2e59ad90cc | ||
| 
						 | 
					1b8c9edcde | ||
| 
						 | 
					d4c2a85f9c | ||
| 
						 | 
					8c75b87e94 | ||
| 
						 | 
					409d4b9d47 | ||
| 
						 | 
					4e3b95d120 | ||
| 
						 | 
					61a9c9fa33 | ||
| 
						 | 
					9c605f2d46 | ||
| 
						 | 
					44bb5a89c8 | ||
| 
						 | 
					cbdb96f105 | ||
| 
						 | 
					9ee3463d07 | ||
| 
						 | 
					f0b14055b6 | ||
| 
						 | 
					fbd9e87b51 | ||
| 
						 | 
					edb3b77916 | ||
| 
						 | 
					ebaa84617f | ||
| 
						 | 
					8eb18995cb | ||
| 
						 | 
					ebabf0e7d8 | ||
| 
						 | 
					607e1f823d | ||
| 
						 | 
					3b52a306cd | ||
| 
						 | 
					0c370d5897 | ||
| 
						 | 
					8bf0448f41 | ||
| 
						 | 
					14e04eb231 | ||
| 
						 | 
					1be9bac3a9 | ||
| 
						 | 
					02b5a3efb8 | ||
| 
						 | 
					bd457f64d8 | ||
| 
						 | 
					9efeea14f2 | ||
| 
						 | 
					9b48ff5775 | ||
| 
						 | 
					117b58ebe6 | ||
| 
						 | 
					303b699005 | ||
| 
						 | 
					9173da0416 | ||
| 
						 | 
					d2cd65f5db | ||
| 
						 | 
					2735f96516 | ||
| 
						 | 
					6847645782 | ||
| 
						 | 
					b0bc898278 | ||
| 
						 | 
					c0f6af7213 | ||
| 
						 | 
					5edebaf468 | ||
| 
						 | 
					d436409153 | ||
| 
						 | 
					8c41fc2b1d | ||
| 
						 | 
					46f17bea66 | ||
| 
						 | 
					11477dbc03 | ||
| 
						 | 
					947c104eff | ||
| 
						 | 
					e5366dbbe7 | ||
| 
						 | 
					d3375193a9 | ||
| 
						 | 
					6144ce1fe0 | ||
| 
						 | 
					1771e673d2 | ||
| 
						 | 
					d258e06fd7 | ||
| 
						 | 
					854f4a8896 | ||
| 
						 | 
					f94c221a9a | ||
| 
						 | 
					6a2f0f5143 | ||
| 
						 | 
					183e2a8471 | ||
| 
						 | 
					c6c2842bdb | ||
| 
						 | 
					f26767b65e | ||
| 
						 | 
					98d32876b5 | ||
| 
						 | 
					e5d0f3c036 | ||
| 
						 | 
					cc15aaacbb | ||
| 
						 | 
					553df1d57b | ||
| 
						 | 
					b92311402a | ||
| 
						 | 
					93796491af | ||
| 
						 | 
					c038cf27a7 | ||
| 
						 | 
					1f42d32eb5 | ||
| 
						 | 
					06bde559da | ||
| 
						 | 
					922f7167f5 | ||
| 
						 | 
					90c0d3e12f | ||
| 
						 | 
					bf5f846fc6 | ||
| 
						 | 
					926bcc71ae | ||
| 
						 | 
					ea4a458214 | ||
| 
						 | 
					b3ae3e1feb | ||
| 
						 | 
					fe7af21c91 | ||
| 
						 | 
					29f72037fe | ||
| 
						 | 
					1d6b4bfcef | ||
| 
						 | 
					5bfac5ec09 | ||
| 
						 | 
					dfffaace26 | ||
| 
						 | 
					1d5f628c7a | ||
| 
						 | 
					cb8a6f66fa | ||
| 
						 | 
					cb21c7c18d | ||
| 
						 | 
					0d104776bc | ||
| 
						 | 
					5f27757039 | ||
| 
						 | 
					532907219b | ||
| 
						 | 
					eeaba74553 | ||
| 
						 | 
					dd637582a4 | ||
| 
						 | 
					b0d12aeea1 | ||
| 
						 | 
					bdbd813455 | ||
| 
						 | 
					a6fac2b175 | ||
| 
						 | 
					5ce923ea90 | ||
| 
						 | 
					29f0508dc2 | ||
| 
						 | 
					3ffa59f0cd | ||
| 
						 | 
					790d6ef94c | ||
| 
						 | 
					7828f48b9a | ||
| 
						 | 
					768c71830b | ||
| 
						 | 
					ceb0564ebf | ||
| 
						 | 
					20f7eb7327 | ||
| 
						 | 
					441d5bd44d | ||
| 
						 | 
					9fa19df2ff | ||
| 
						 | 
					39f64f597e | ||
| 
						 | 
					160429eb24 | ||
| 
						 | 
					6516c64e67 | ||
| 
						 | 
					4c8a703084 | ||
| 
						 | 
					335210d788 | ||
| 
						 | 
					9b04e657db | ||
| 
						 | 
					f7311aa025 | ||
| 
						 | 
					fb24e55c8d | ||
| 
						 | 
					b58ca46a46 | ||
| 
						 | 
					76991cdcc4 | ||
| 
						 | 
					69c7cf783e | ||
| 
						 | 
					f751c3828e | ||
| 
						 | 
					5c65f9f9ad | ||
| 
						 | 
					81ae6709e4 | ||
| 
						 | 
					593a3d48fb | ||
| 
						 | 
					a8b90283d8 | ||
| 
						 | 
					80076f935d | ||
| 
						 | 
					9fbb3659a6 | ||
| 
						 | 
					fee446c28a | ||
| 
						 | 
					1d56f0b035 | ||
| 
						 | 
					34e8979d40 | ||
| 
						 | 
					2966a62429 | ||
| 
						 | 
					5983ccc55c | ||
| 
						 | 
					de382b704c | ||
| 
						 | 
					16dbbfabc6 | ||
| 
						 | 
					af8d04818d | ||
| 
						 | 
					ee19ef1aac | ||
| 
						 | 
					5e2d4e332a | ||
| 
						 | 
					c6c857dfff | ||
| 
						 | 
					8dbac20f8b | ||
| 
						 | 
					341fddb9aa | ||
| 
						 | 
					456824669f | ||
| 
						 | 
					62f3039d82 | ||
| 
						 | 
					be4c718859 | ||
| 
						 | 
					c2f9ed7c59 | ||
| 
						 | 
					bfac6607d1 | ||
| 
						 | 
					513066ba52 | ||
| 
						 | 
					316777f757 | ||
| 
						 | 
					246950159d | ||
| 
						 | 
					31d6a54b06 | ||
| 
						 | 
					5c3a6164bb | ||
| 
						 | 
					1652914d39 | ||
| 
						 | 
					618cfd9ec5 | ||
| 
						 | 
					f97cfe9916 | ||
| 
						 | 
					b9259a0238 | ||
| 
						 | 
					5abbe385c5 | ||
| 
						 | 
					e43dcded62 | ||
| 
						 | 
					887081fd71 | ||
| 
						 | 
					71ded24fce | ||
| 
						 | 
					1e2a9e8348 | ||
| 
						 | 
					64a3aa7092 | ||
| 
						 | 
					fda8dd4ce3 | ||
| 
						 | 
					1efabd27d8 | ||
| 
						 | 
					caa651e55b | ||
| 
						 | 
					b0a3891498 | ||
| 
						 | 
					2a9e3d84fd | ||
| 
						 | 
					a3dcac62f9 | ||
| 
						 | 
					6b535b11f8 | ||
| 
						 | 
					d9f09a7523 | ||
| 
						 | 
					159744e09e | ||
| 
						 | 
					c2637a76f7 | ||
| 
						 | 
					237edd75d1 | ||
| 
						 | 
					a34d5e3901 | ||
| 
						 | 
					1dd43a75f2 | ||
| 
						 | 
					1f5cbca509 | ||
| 
						 | 
					3749c11f21 | ||
| 
						 | 
					66cdb761dc | ||
| 
						 | 
					f0d9ad6a4e | ||
| 
						 | 
					03e317d052 | ||
| 
						 | 
					ba461e51a8 | ||
| 
						 | 
					80949521b6 | ||
| 
						 | 
					acbb8e9fd0 | ||
| 
						 | 
					90394a50df | ||
| 
						 | 
					5379794f16 | ||
| 
						 | 
					0a32321c85 | ||
| 
						 | 
					c9062599df | ||
| 
						 | 
					10a6e9b4ee | ||
| 
						 | 
					4b8ec44262 | ||
| 
						 | 
					bd74ed4bc0 | ||
| 
						 | 
					fc42f14448 | ||
| 
						 | 
					d01f296420 | ||
| 
						 | 
					27112e2ace | ||
| 
						 | 
					837930234f | ||
| 
						 | 
					e19aa3bbe0 | ||
| 
						 | 
					35b5c1ed56 | ||
| 
						 | 
					3e65e6c69a | ||
| 
						 | 
					3b3297d269 | ||
| 
						 | 
					fc0deb642a | ||
| 
						 | 
					9f2b2f51ff | ||
| 
						 | 
					c9d93ff685 | ||
| 
						 | 
					fa72990a63 | ||
| 
						 | 
					e5afb1c4ea | ||
| 
						 | 
					73ead5f328 | ||
| 
						 | 
					5c57b51378 | ||
| 
						 | 
					e25935ef21 | ||
| 
						 | 
					c7a52c3894 | ||
| 
						 | 
					01a4b4e82f | ||
| 
						 | 
					766866197b | ||
| 
						 | 
					9b5a3cbcd3 | ||
| 
						 | 
					d2ed3b9bec | ||
| 
						 | 
					99d2db42cd | ||
| 
						 | 
					7619507e6c | ||
| 
						 | 
					53a4689ed1 | ||
| 
						 | 
					0a82e6e792 | ||
| 
						 | 
					98855e4123 | ||
| 
						 | 
					71d9d64a02 | ||
| 
						 | 
					6a09d7c49b | ||
| 
						 | 
					46e50ba53f | ||
| 
						 | 
					f1e3ff2ed2 | ||
| 
						 | 
					7787fa8f29 | ||
| 
						 | 
					70902029f8 | ||
| 
						 | 
					4f9a56c884 | ||
| 
						 | 
					3715ba030b | ||
| 
						 | 
					2e49fd7b48 | ||
| 
						 | 
					06912b492f | ||
| 
						 | 
					442e58b07a | ||
| 
						 | 
					799f04efc0 | ||
| 
						 | 
					cc7dbeada6 | ||
| 
						 | 
					45d368e3a1 | ||
| 
						 | 
					628a94bad3 | ||
| 
						 | 
					0c93be97a9 | ||
| 
						 | 
					54eb6070fb | ||
| 
						 | 
					4dbf1c521e | ||
| 
						 | 
					0651716b96 | ||
| 
						 | 
					f30b8f6b3c | ||
| 
						 | 
					0992609bf4 | ||
| 
						 | 
					18c08f24ad | ||
| 
						 | 
					a7f53aea0e | ||
| 
						 | 
					c399905675 | ||
| 
						 | 
					5cb0c11feb | ||
| 
						 | 
					08b67e7aea | ||
| 
						 | 
					07ae8ec553 | ||
| 
						 | 
					04c3a43c17 | ||
| 
						 | 
					b632344596 | ||
| 
						 | 
					fb8ec79a52 | ||
| 
						 | 
					7dd16df846 | ||
| 
						 | 
					551e9c6111 | ||
| 
						 | 
					cc9f0b3f47 | ||
| 
						 | 
					d77c3abdc0 | ||
| 
						 | 
					dd37a4e04c | ||
| 
						 | 
					1f5c79bd17 | ||
| 
						 | 
					623570a117 | ||
| 
						 | 
					cdbc146e5d | ||
| 
						 | 
					7ae611256a | ||
| 
						 | 
					b62c47fede | ||
| 
						 | 
					99f497c3b8 | ||
| 
						 | 
					4f88c2489b | ||
| 
						 | 
					294ba1fca7 | ||
| 
						 | 
					be61b38a2c | ||
| 
						 | 
					f9797825ad | ||
| 
						 | 
					fd4b7d4588 | ||
| 
						 | 
					062cedc200 | ||
| 
						 | 
					79b9d0579d | ||
| 
						 | 
					ab31117bf3 | ||
| 
						 | 
					d31040f5d8 | ||
| 
						 | 
					52d19fa43d | ||
| 
						 | 
					8ca34f7098 | ||
| 
						 | 
					4c4099966a | ||
| 
						 | 
					86ac7f3a59 | ||
| 
						 | 
					9e400a7857 | ||
| 
						 | 
					d5278351da | ||
| 
						 | 
					36861595f1 | ||
| 
						 | 
					d604321f37 | ||
| 
						 | 
					964ab65497 | ||
| 
						 | 
					3b940b1c04 | ||
| 
						 | 
					a91e6a6bdf | ||
| 
						 | 
					8600620305 | ||
| 
						 | 
					96721f305f | ||
| 
						 | 
					2bf70d7d00 | ||
| 
						 | 
					1d8c170f48 | ||
| 
						 | 
					6009c7edb4 | ||
| 
						 | 
					e3f36c033e | ||
| 
						 | 
					d4eb0f1655 | ||
| 
						 | 
					5fca480921 | ||
| 
						 | 
					7051f897bc | ||
| 
						 | 
					2cb3015a28 | ||
| 
						 | 
					d0859a7d33 | ||
| 
						 | 
					e20ec00071 | ||
| 
						 | 
					150114d774 | ||
| 
						 | 
					89dfa5ea82 | ||
| 
						 | 
					61ebc629f6 | ||
| 
						 | 
					32f2da77f8 | ||
| 
						 | 
					bfca3f242a | ||
| 
						 | 
					3dfff2930a | ||
| 
						 | 
					027e0de48e | ||
| 
						 | 
					c811141a4f | ||
| 
						 | 
					871c0ee2a5 | ||
| 
						 | 
					b8a7741c61 | ||
| 
						 | 
					97aa930ad2 | ||
| 
						 | 
					2a5def10e7 | ||
| 
						 | 
					969834e037 | ||
| 
						 | 
					d73a44c504 | ||
| 
						 | 
					8aec092ab6 | ||
| 
						 | 
					4fa959ba45 | ||
| 
						 | 
					b6011b9353 | ||
| 
						 | 
					40a5005d94 | ||
| 
						 | 
					c5eba21ff6 | ||
| 
						 | 
					4891cfef56 | ||
| 
						 | 
					4395664547 | ||
| 
						 | 
					04d926af39 | ||
| 
						 | 
					f9a31c1abb | ||
| 
						 | 
					b43712d78d | ||
| 
						 | 
					01904a0f10 | ||
| 
						 | 
					dd875e7529 | ||
| 
						 | 
					f1dcf0f0b8 | ||
| 
						 | 
					a045d001bf | ||
| 
						 | 
					066c1022d0 | ||
| 
						 | 
					dca1c0f160 | ||
| 
						 | 
					2419bc3678 | ||
| 
						 | 
					c19b3ecd43 | ||
| 
						 | 
					ef1e91d838 | ||
| 
						 | 
					e5929225eb | ||
| 
						 | 
					59c192becc | ||
| 
						 | 
					a800816750 | ||
| 
						 | 
					99d9ab4e40 | ||
| 
						 | 
					f310ca1b74 | ||
| 
						 | 
					f763daa577 | ||
| 
						 | 
					607c3ae651 | ||
| 
						 | 
					970563e07b | ||
| 
						 | 
					e006045f59 | ||
| 
						 | 
					fff3645901 | ||
| 
						 | 
					a5383fd208 | ||
| 
						 | 
					5591832b50 | ||
| 
						 | 
					9ce3a2059f | ||
| 
						 | 
					69e6cf2c0c | ||
| 
						 | 
					28635124f9 | ||
| 
						 | 
					25b116048c | ||
| 
						 | 
					035be87a83 | ||
| 
						 | 
					e8bdbc45a9 | ||
| 
						 | 
					429caccefa | ||
| 
						 | 
					744ca1af7c | ||
| 
						 | 
					106f0d611f | ||
| 
						 | 
					d826416684 | ||
| 
						 | 
					24ba9eba46 | ||
| 
						 | 
					424c34225f | ||
| 
						 | 
					d781f3a11b | ||
| 
						 | 
					a80f9ed336 | ||
| 
						 | 
					92bbedfa5a | ||
| 
						 | 
					86710ed483 | ||
| 
						 | 
					da7eb9ac90 | ||
| 
						 | 
					c411043681 | ||
| 
						 | 
					93f8ee7e60 | ||
| 
						 | 
					0efc1f06f2 | ||
| 
						 | 
					81959804df | ||
| 
						 | 
					75b524ddc4 | ||
| 
						 | 
					f599c36272 | ||
| 
						 | 
					9bb64315f3 | ||
| 
						 | 
					575badc690 | ||
| 
						 | 
					4b91cfb7f9 | ||
| 
						 | 
					9ad9d64ac7 | ||
| 
						 | 
					5a2cfa2798 | ||
| 
						 | 
					eb24da7c82 | ||
| 
						 | 
					f93e261d75 | ||
| 
						 | 
					501f88ca86 | ||
| 
						 | 
					360effcb72 | ||
| 
						 | 
					eb9bd69405 | ||
| 
						 | 
					11b8210e36 | ||
| 
						 | 
					a57a842f7b | ||
| 
						 | 
					a8c253a2a5 | ||
| 
						 | 
					8b737aabd9 | ||
| 
						 | 
					0db4815f3d | ||
| 
						 | 
					139db58a66 | ||
| 
						 | 
					d23376b81e | ||
| 
						 | 
					99d90845b5 | ||
| 
						 | 
					ea0127e42b | ||
| 
						 | 
					c32fec7432 | ||
| 
						 | 
					8bd23dd457 | ||
| 
						 | 
					97a12c0169 | ||
| 
						 | 
					635916737b | ||
| 
						 | 
					65c50e4f01 | ||
| 
						 | 
					5cf18235e3 | ||
| 
						 | 
					b80f3fdec9 | ||
| 
						 | 
					0426be9280 | ||
| 
						 | 
					7c678659d4 | ||
| 
						 | 
					13fe9e83fa | ||
| 
						 | 
					4711f36a1f | ||
| 
						 | 
					01e2a51132 | ||
| 
						 | 
					a70a205ace | ||
| 
						 | 
					33625e2dd3 | ||
| 
						 | 
					0277218319 | ||
| 
						 | 
					18a8c727fa | ||
| 
						 | 
					80ad784a4e | ||
| 
						 | 
					ebadaa9660 | ||
| 
						 | 
					7bc51582f0 | ||
| 
						 | 
					11fb54c74e | ||
| 
						 | 
					913ac8b7e8 | ||
| 
						 | 
					2b9350ce76 | ||
| 
						 | 
					3b18f1b87f | ||
| 
						 | 
					c5c24c1989 | ||
| 
						 | 
					c3938d04f3 | ||
| 
						 | 
					f968713be8 | ||
| 
						 | 
					7b11284008 | ||
| 
						 | 
					f39c0d52ee | ||
| 
						 | 
					a3756a9600 | ||
| 
						 | 
					afa436fe8f | ||
| 
						 | 
					48b5ea9e59 | ||
| 
						 | 
					56974153f1 | ||
| 
						 | 
					9a2cd71571 | ||
| 
						 | 
					d1c6368283 | ||
| 
						 | 
					5c3268b8d4 | ||
| 
						 | 
					25af5ab7c6 | ||
| 
						 | 
					4d586b1446 | ||
| 
						 | 
					bb759d52c8 | ||
| 
						 | 
					9a2cf05c5f | ||
| 
						 | 
					c79d700d03 | ||
| 
						 | 
					482a3aebc9 | ||
| 
						 | 
					387f249363 | ||
| 
						 | 
					3d917d0b7e | ||
| 
						 | 
					824f3187ac | ||
| 
						 | 
					e3c27a483c | ||
| 
						 | 
					a33bb32874 | ||
| 
						 | 
					93d9d4b50a | ||
| 
						 | 
					2376a2c941 | ||
| 
						 | 
					b92702a312 | ||
| 
						 | 
					b11d5f6799 | ||
| 
						 | 
					072dce340e | ||
| 
						 | 
					cccb1a2c9e | ||
| 
						 | 
					063d9c47a4 | ||
| 
						 | 
					8d8d421286 | ||
| 
						 | 
					0ce57e5a39 | ||
| 
						 | 
					aebad04c0b | ||
| 
						 | 
					514d11d46f | ||
| 
						 | 
					96e46db272 | ||
| 
						 | 
					76f78877f6 | ||
| 
						 | 
					4ffa68b773 | ||
| 
						 | 
					8eaffee160 | ||
| 
						 | 
					557a622f71 | ||
| 
						 | 
					e9c6556296 | ||
| 
						 | 
					dce9d59dfe | ||
| 
						 | 
					d3e291b442 | ||
| 
						 | 
					d4686c0fb1 | ||
| 
						 | 
					ae9b247f47 | ||
| 
						 | 
					a59761d292 | ||
| 
						 | 
					030c87d142 | ||
| 
						 | 
					95ed3e9d46 | ||
| 
						 | 
					d0eaebe19f | ||
| 
						 | 
					9ecead2645 | ||
| 
						 | 
					98166dfa66 | ||
| 
						 | 
					5645be4e0f | ||
| 
						 | 
					9a7a205510 | ||
| 
						 | 
					7e3b8fd346 | ||
| 
						 | 
					3d6dcc9eee | ||
| 
						 | 
					4f6982fbc5 | ||
| 
						 | 
					00c144daeb | ||
| 
						 | 
					54660300e9 | ||
| 
						 | 
					5dc40049be | ||
| 
						 | 
					1e46b4073f | ||
| 
						 | 
					29fc4af0fc | ||
| 
						 | 
					4030a2e253 | ||
| 
						 | 
					ea80cb751b | ||
| 
						 | 
					6aa61dbce7 | ||
| 
						 | 
					cdc9c99d40 | ||
| 
						 | 
					a546677b08 | ||
| 
						 | 
					5c3af1d3f6 | ||
| 
						 | 
					66b0b6feeb | ||
| 
						 | 
					7665a220a0 | ||
| 
						 | 
					4250af4dd9 | ||
| 
						 | 
					73252ccd25 | ||
| 
						 | 
					33bf78c369 | ||
| 
						 | 
					96ded4e402 | ||
| 
						 | 
					076124eb71 | ||
| 
						 | 
					b660e5a7fa | ||
| 
						 | 
					3b4ea0ed0a | ||
| 
						 | 
					403b6e32e3 | ||
| 
						 | 
					b91a1aa027 | ||
| 
						 | 
					13dbdd9b16 | ||
| 
						 | 
					37bc0b3b5a | ||
| 
						 | 
					be70a96651 | ||
| 
						 | 
					d83d214497 | ||
| 
						 | 
					6ec0f80b76 | ||
| 
						 | 
					06f566346d | ||
| 
						 | 
					b680649113 | ||
| 
						 | 
					5ab2ef4079 | ||
| 
						 | 
					392ed64375 | ||
| 
						 | 
					566c129435 | ||
| 
						 | 
					c903eb2d01 | ||
| 
						 | 
					69c78651d5 | ||
| 
						 | 
					34487c9de4 | ||
| 
						 | 
					822377be8b | 
@@ -49,7 +49,7 @@ ConstructorInitializerAllOnOneLineOrOnePerLine: true
 | 
			
		||||
ConstructorInitializerIndentWidth: 4
 | 
			
		||||
ContinuationIndentWidth: 4
 | 
			
		||||
Cpp11BracedListStyle: true
 | 
			
		||||
DerivePointerAlignment: true
 | 
			
		||||
DerivePointerAlignment: false
 | 
			
		||||
DisableFormat:   false
 | 
			
		||||
ExperimentalAutoDetectBinPacking: false
 | 
			
		||||
FixNamespaceComments: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								.clang-tidy
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								.clang-tidy
									
									
									
									
									
								
							@@ -4,14 +4,29 @@ Checks: >-
 | 
			
		||||
  -abseil-*,
 | 
			
		||||
  -android-*,
 | 
			
		||||
  -boost-*,
 | 
			
		||||
  -bugprone-macro-parentheses,
 | 
			
		||||
  -bugprone-branch-clone,
 | 
			
		||||
  -bugprone-narrowing-conversions,
 | 
			
		||||
  -bugprone-signed-char-misuse,
 | 
			
		||||
  -bugprone-too-small-loop-variable,
 | 
			
		||||
  -cert-dcl50-cpp,
 | 
			
		||||
  -cert-err58-cpp,
 | 
			
		||||
  -clang-analyzer-core.CallAndMessage,
 | 
			
		||||
  -cert-oop57-cpp,
 | 
			
		||||
  -cert-str34-c,
 | 
			
		||||
  -clang-analyzer-optin.cplusplus.UninitializedObject,
 | 
			
		||||
  -clang-analyzer-osx.*,
 | 
			
		||||
  -clang-analyzer-security.*,
 | 
			
		||||
  -clang-diagnostic-delete-abstract-non-virtual-dtor,
 | 
			
		||||
  -clang-diagnostic-delete-non-abstract-non-virtual-dtor,
 | 
			
		||||
  -clang-diagnostic-shadow-field,
 | 
			
		||||
  -clang-diagnostic-sign-compare,
 | 
			
		||||
  -clang-diagnostic-unused-variable,
 | 
			
		||||
  -clang-diagnostic-unused-const-variable,
 | 
			
		||||
  -cppcoreguidelines-avoid-c-arrays,
 | 
			
		||||
  -cppcoreguidelines-avoid-goto,
 | 
			
		||||
  -cppcoreguidelines-c-copy-assignment-signature,
 | 
			
		||||
  -cppcoreguidelines-avoid-magic-numbers,
 | 
			
		||||
  -cppcoreguidelines-init-variables,
 | 
			
		||||
  -cppcoreguidelines-macro-usage,
 | 
			
		||||
  -cppcoreguidelines-narrowing-conversions,
 | 
			
		||||
  -cppcoreguidelines-non-private-member-variables-in-classes,
 | 
			
		||||
  -cppcoreguidelines-owning-memory,
 | 
			
		||||
  -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
 | 
			
		||||
  -cppcoreguidelines-pro-bounds-constant-array-index,
 | 
			
		||||
@@ -24,42 +39,52 @@ Checks: >-
 | 
			
		||||
  -cppcoreguidelines-pro-type-union-access,
 | 
			
		||||
  -cppcoreguidelines-pro-type-vararg,
 | 
			
		||||
  -cppcoreguidelines-special-member-functions,
 | 
			
		||||
  -fuchsia-*,
 | 
			
		||||
  -fuchsia-default-arguments,
 | 
			
		||||
  -fuchsia-multiple-inheritance,
 | 
			
		||||
  -fuchsia-overloaded-operator,
 | 
			
		||||
  -fuchsia-statically-constructed-objects,
 | 
			
		||||
  -fuchsia-default-arguments-declarations,
 | 
			
		||||
  -fuchsia-default-arguments-calls,
 | 
			
		||||
  -google-build-using-namespace,
 | 
			
		||||
  -google-explicit-constructor,
 | 
			
		||||
  -google-readability-braces-around-statements,
 | 
			
		||||
  -google-readability-casting,
 | 
			
		||||
  -google-readability-todo,
 | 
			
		||||
  -google-runtime-int,
 | 
			
		||||
  -google-runtime-references,
 | 
			
		||||
  -hicpp-*,
 | 
			
		||||
  -llvm-else-after-return,
 | 
			
		||||
  -llvm-header-guard,
 | 
			
		||||
  -llvm-include-order,
 | 
			
		||||
  -misc-unconventional-assign-operator,
 | 
			
		||||
  -llvm-qualified-auto,
 | 
			
		||||
  -llvmlibc-*,
 | 
			
		||||
  -misc-non-private-member-variables-in-classes,
 | 
			
		||||
  -misc-no-recursion,
 | 
			
		||||
  -misc-unused-parameters,
 | 
			
		||||
  -modernize-deprecated-headers,
 | 
			
		||||
  -modernize-pass-by-value,
 | 
			
		||||
  -modernize-pass-by-value,
 | 
			
		||||
  -modernize-avoid-c-arrays,
 | 
			
		||||
  -modernize-return-braced-init-list,
 | 
			
		||||
  -modernize-use-auto,
 | 
			
		||||
  -modernize-use-default-member-init,
 | 
			
		||||
  -modernize-use-equals-default,
 | 
			
		||||
  -modernize-use-trailing-return-type,
 | 
			
		||||
  -mpi-*,
 | 
			
		||||
  -objc-*,
 | 
			
		||||
  -performance-unnecessary-value-param,
 | 
			
		||||
  -readability-braces-around-statements,
 | 
			
		||||
  -readability-const-return-type,
 | 
			
		||||
  -readability-convert-member-functions-to-static,
 | 
			
		||||
  -readability-else-after-return,
 | 
			
		||||
  -readability-implicit-bool-conversion,
 | 
			
		||||
  -readability-isolate-declaration,
 | 
			
		||||
  -readability-magic-numbers,
 | 
			
		||||
  -readability-make-member-function-const,
 | 
			
		||||
  -readability-named-parameter,
 | 
			
		||||
  -readability-qualified-auto,
 | 
			
		||||
  -readability-redundant-access-specifiers,
 | 
			
		||||
  -readability-redundant-member-init,
 | 
			
		||||
  -warnings-as-errors,
 | 
			
		||||
  -zircon-*
 | 
			
		||||
  -readability-redundant-string-init,
 | 
			
		||||
  -readability-uppercase-literal-suffix,
 | 
			
		||||
  -readability-use-anyofallof,
 | 
			
		||||
  -warnings-as-errors
 | 
			
		||||
WarningsAsErrors: '*'
 | 
			
		||||
HeaderFilterRegex: '^.*/src/esphome/.*'
 | 
			
		||||
AnalyzeTemporaryDtors: false
 | 
			
		||||
FormatStyle:     google
 | 
			
		||||
CheckOptions:
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,29 @@
 | 
			
		||||
  "name": "ESPHome Dev",
 | 
			
		||||
  "context": "..",
 | 
			
		||||
  "dockerFile": "../docker/Dockerfile.dev",
 | 
			
		||||
  "postCreateCommand": "mkdir -p config && pip3 install -e .",
 | 
			
		||||
  "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"],
 | 
			
		||||
  "postCreateCommand": [
 | 
			
		||||
    "script/devcontainer-post-create"
 | 
			
		||||
  ],
 | 
			
		||||
  "runArgs": [
 | 
			
		||||
    "--privileged",
 | 
			
		||||
    "-e",
 | 
			
		||||
    "ESPHOME_DASHBOARD_USE_PING=1"
 | 
			
		||||
  ],
 | 
			
		||||
  "appPort": 6052,
 | 
			
		||||
  "extensions": [
 | 
			
		||||
    // python
 | 
			
		||||
    "ms-python.python",
 | 
			
		||||
    "visualstudioexptteam.vscodeintellicode",
 | 
			
		||||
    "redhat.vscode-yaml"
 | 
			
		||||
    // yaml
 | 
			
		||||
    "redhat.vscode-yaml",
 | 
			
		||||
    // cpp
 | 
			
		||||
    "ms-vscode.cpptools",
 | 
			
		||||
    // editorconfig
 | 
			
		||||
    "editorconfig.editorconfig",
 | 
			
		||||
  ],
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "python.pythonPath": "/usr/local/bin/python",
 | 
			
		||||
    "python.languageServer": "Pylance",
 | 
			
		||||
    "python.pythonPath": "/usr/bin/python3",
 | 
			
		||||
    "python.linting.pylintEnabled": true,
 | 
			
		||||
    "python.linting.enabled": true,
 | 
			
		||||
    "python.formatting.provider": "black",
 | 
			
		||||
@@ -19,7 +32,7 @@
 | 
			
		||||
    "editor.formatOnSave": true,
 | 
			
		||||
    "editor.formatOnType": true,
 | 
			
		||||
    "files.trimTrailingWhitespace": true,
 | 
			
		||||
    "terminal.integrated.shell.linux": "/bin/bash",
 | 
			
		||||
    "terminal.integrated.defaultProfile.linux": "bash",
 | 
			
		||||
    "yaml.customTags": [
 | 
			
		||||
      "!secret scalar",
 | 
			
		||||
      "!lambda scalar",
 | 
			
		||||
@@ -27,6 +40,18 @@
 | 
			
		||||
      "!include_dir_list scalar",
 | 
			
		||||
      "!include_dir_merge_list scalar",
 | 
			
		||||
      "!include_dir_merge_named scalar"
 | 
			
		||||
    ]
 | 
			
		||||
    ],
 | 
			
		||||
    "files.exclude": {
 | 
			
		||||
      "**/.git": true,
 | 
			
		||||
      "**/.DS_Store": true,
 | 
			
		||||
      "**/*.pyc": {
 | 
			
		||||
        "when": "$(basename).py"
 | 
			
		||||
      },
 | 
			
		||||
      "**/__pycache__": true
 | 
			
		||||
    },
 | 
			
		||||
    "files.associations": {
 | 
			
		||||
      "**/.vscode/*.json": "jsonc"
 | 
			
		||||
    },
 | 
			
		||||
    "C_Cpp.clang_format_path": "/usr/bin/clang-format-11",
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -103,6 +103,10 @@ venv.bak/
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
 | 
			
		||||
# PlatformIO
 | 
			
		||||
.pio/
 | 
			
		||||
 | 
			
		||||
# ESPHome
 | 
			
		||||
config/
 | 
			
		||||
examples/
 | 
			
		||||
Dockerfile
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ insert_final_newline = true
 | 
			
		||||
charset = utf-8
 | 
			
		||||
 | 
			
		||||
# python
 | 
			
		||||
[*.{py}]
 | 
			
		||||
[*.py]
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 4
 | 
			
		||||
 | 
			
		||||
@@ -25,4 +25,10 @@ indent_size = 2
 | 
			
		||||
[*.{yaml,yml}]
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 2
 | 
			
		||||
quote_type = single
 | 
			
		||||
quote_type = single
 | 
			
		||||
 | 
			
		||||
# JSON
 | 
			
		||||
[*.json]
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 2
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							@@ -1,25 +1,22 @@
 | 
			
		||||
# What does this implement/fix? 
 | 
			
		||||
 | 
			
		||||
Quick description 
 | 
			
		||||
Quick description and explanation of changes
 | 
			
		||||
 | 
			
		||||
## Types of changes
 | 
			
		||||
 | 
			
		||||
- [ ] Bugfix (non-breaking change which fixes an issue)
 | 
			
		||||
- [ ] New feature (non-breaking change which adds functionality)
 | 
			
		||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
 | 
			
		||||
- [ ] Configuration change (this will require users to update their yaml configuration files to keep working)
 | 
			
		||||
- [ ] Other
 | 
			
		||||
 | 
			
		||||
**Related issue or feature (if applicable):** fixes <link to issue>
 | 
			
		||||
 | 
			
		||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>
 | 
			
		||||
  
 | 
			
		||||
# Test Environment
 | 
			
		||||
 | 
			
		||||
## Test Environment
 | 
			
		||||
 | 
			
		||||
- [ ] ESP32
 | 
			
		||||
- [ ] ESP8266
 | 
			
		||||
- [ ] Windows
 | 
			
		||||
- [ ] Mac OS
 | 
			
		||||
- [ ] Linux
 | 
			
		||||
 | 
			
		||||
## Example entry for `config.yaml`:
 | 
			
		||||
<!--
 | 
			
		||||
@@ -34,11 +31,6 @@ Quick description
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Explain your changes
 | 
			
		||||
 | 
			
		||||
Describe your changes here to communicate to the maintainers **why we should accept this pull request**.
 | 
			
		||||
Very important to fill if no issue linked
 | 
			
		||||
 | 
			
		||||
## Checklist:
 | 
			
		||||
  - [ ] The code change is tested and works locally.
 | 
			
		||||
  - [ ] Tests have been added to verify that the new code works (under `tests/` folder).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										49
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,7 @@ name: CI for docker images
 | 
			
		||||
# Only run when docker paths change
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [dev, beta, master]
 | 
			
		||||
    branches: [dev, beta, release]
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docker/**'
 | 
			
		||||
      - '.github/workflows/**'
 | 
			
		||||
@@ -18,38 +18,23 @@ jobs:
 | 
			
		||||
    name: Build docker containers
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        build_type: ["hassio", "docker"]
 | 
			
		||||
        build_type: ["ha-addon", "docker", "lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up env variables
 | 
			
		||||
        run: |
 | 
			
		||||
          base_version="3.0.0"
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "TAG=check" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
          if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
 | 
			
		||||
            build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-hassio-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile.hassio"
 | 
			
		||||
          else
 | 
			
		||||
            build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV
 | 
			
		||||
          echo "BUILD_TO=${build_to}" >> $GITHUB_ENV
 | 
			
		||||
          echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV
 | 
			
		||||
      - name: Pull for cache
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull "${BUILD_TO}:dev" || true
 | 
			
		||||
      - name: Register QEMU binfmt
 | 
			
		||||
        run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker build \
 | 
			
		||||
            --build-arg "BUILD_FROM=${BUILD_FROM}" \
 | 
			
		||||
            --build-arg "BUILD_VERSION=ci" \
 | 
			
		||||
            --cache-from "${BUILD_TO}:dev" \
 | 
			
		||||
            --file "${DOCKERFILE}" \
 | 
			
		||||
            .
 | 
			
		||||
    - name: Run build
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${TAG}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          build
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										193
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										193
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -4,67 +4,98 @@ name: CI
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    # On dev branch release-dev already performs CI checks
 | 
			
		||||
    # On other branches the `pull_request` trigger will be used
 | 
			
		||||
    branches: [beta, master]
 | 
			
		||||
    branches: [dev, beta, release]
 | 
			
		||||
 | 
			
		||||
  pull_request:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint-clang-format:
 | 
			
		||||
  ci-with-container:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-format
 | 
			
		||||
        run: script/clang-format -i
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
 | 
			
		||||
  lint-clang-tidy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        split: [1, 2, 3, 4]
 | 
			
		||||
        include:
 | 
			
		||||
          - id: clang-format
 | 
			
		||||
            name: Run script/clang-format
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy 1/4
 | 
			
		||||
            split: 1
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy 2/4
 | 
			
		||||
            split: 2
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy 3/4
 | 
			
		||||
            split: 3
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy 4/4
 | 
			
		||||
            split: 4
 | 
			
		||||
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: ghcr.io/esphome/esphome-lint:1.1
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
 | 
			
		||||
      # Also run git-diff-index so that the step is marked as failed on formatting errors,
 | 
			
		||||
      # since clang-format doesn't do anything but change files if -i is passed.
 | 
			
		||||
      - name: Run clang-format
 | 
			
		||||
        run: |
 | 
			
		||||
          script/clang-format -i
 | 
			
		||||
          git diff-index --quiet HEAD --
 | 
			
		||||
        if: ${{ matrix.id == 'clang-format' }}
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }}
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: ${{ matrix.id == 'clang-tidy' }}
 | 
			
		||||
 | 
			
		||||
  lint-python:
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  ci:
 | 
			
		||||
    # Don't use the esphome-lint docker image because it may contain outdated requirements.
 | 
			
		||||
    # This way, all dependencies are cached via the cache action.
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - id: ci-custom
 | 
			
		||||
            name: Run script/ci-custom
 | 
			
		||||
          - id: lint-python
 | 
			
		||||
            name: Run script/lint-python
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test1.yaml
 | 
			
		||||
            name: Test tests/test1.yaml
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test2.yaml
 | 
			
		||||
            name: Test tests/test2.yaml
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test3.yaml
 | 
			
		||||
            name: Test tests/test3.yaml
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test4.yaml
 | 
			
		||||
            name: Test tests/test4.yaml
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test5.yaml
 | 
			
		||||
            name: Test tests/test5.yaml
 | 
			
		||||
          - id: pytest
 | 
			
		||||
            name: Run pytest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
@@ -72,6 +103,17 @@ jobs:
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
 | 
			
		||||
      # Use per test platformio cache because tests have different platform versions
 | 
			
		||||
      - name: Cache ~/.platformio
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: test-home-platformio-${{ matrix.file }}-${{ hashFiles('esphome/core/config.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            test-home-platformio-${{ matrix.file }}-
 | 
			
		||||
        if: ${{ matrix.id == 'test' }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
 | 
			
		||||
@@ -80,81 +122,22 @@ jobs:
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/lint-python.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/pytest.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
 | 
			
		||||
      - name: Lint Custom
 | 
			
		||||
        run: script/ci-custom.py
 | 
			
		||||
        run: |
 | 
			
		||||
          script/ci-custom.py
 | 
			
		||||
          script/build_codeowners.py --check
 | 
			
		||||
        if: ${{ matrix.id == 'ci-custom' }}
 | 
			
		||||
      - name: Lint Python
 | 
			
		||||
        run: script/lint-python
 | 
			
		||||
      - name: Lint CODEOWNERS
 | 
			
		||||
        run: script/build_codeowners.py --check
 | 
			
		||||
        if: ${{ matrix.id == 'lint-python' }}
 | 
			
		||||
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
          test:
 | 
			
		||||
          - test1
 | 
			
		||||
          - test2
 | 
			
		||||
          - test3
 | 
			
		||||
          - test4
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      # Use per test platformio cache because tests have different platform versions
 | 
			
		||||
      - name: Cache ~/.platformio
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            test-home-platformio-${{ matrix.test }}-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - run: esphome compile ${{ matrix.file }}
 | 
			
		||||
        if: ${{ matrix.id == 'test' }}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - run: esphome tests/${{ matrix.test }}.yaml compile
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - name: Install Github Actions annotator
 | 
			
		||||
        run: pip install pytest-github-actions-annotate-failures
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        run: |
 | 
			
		||||
          pytest \
 | 
			
		||||
            -qq \
 | 
			
		||||
            --durations=10 \
 | 
			
		||||
            -o console_output_style=count \
 | 
			
		||||
            tests
 | 
			
		||||
          pytest -vv --tb=native tests
 | 
			
		||||
        if: ${{ matrix.id == 'pytest' }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										104
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										104
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							@@ -7,30 +7,94 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docker/Dockerfile.lint'
 | 
			
		||||
      - 'requirements.txt'
 | 
			
		||||
      - 'requirements_optional.txt'
 | 
			
		||||
      - 'requirements_test.txt'
 | 
			
		||||
      - 'platformio.ini'
 | 
			
		||||
      - '.github/workflows/docker-lint-build.yml'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  publish-docker-lint-iage:
 | 
			
		||||
    name: Build docker containers
 | 
			
		||||
  deploy-docker:
 | 
			
		||||
    name: Build and publish docker containers
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        build_type: ["lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Pull for cache
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull "esphome/esphome-lint:latest" || true
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: |
 | 
			
		||||
          docker build \
 | 
			
		||||
            --cache-from "esphome/esphome-lint:latest" \
 | 
			
		||||
            --file "docker/Dockerfile.lint" \
 | 
			
		||||
            --tag "esphome/esphome-lint:latest" \
 | 
			
		||||
            .
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_USER: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
        run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker push "esphome/esphome-lint:latest"
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "TAG=1.1" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
    - name: Run build
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${TAG}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          build
 | 
			
		||||
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
        username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
    - name: Log in to the GitHub container registry
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
    - name: Run push
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${TAG}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          push
 | 
			
		||||
 | 
			
		||||
  deploy-docker-manifest:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        build_type: ["lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "TAG=1.1" >> $GITHUB_ENV
 | 
			
		||||
    - name: Enable experimental manifest support
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p ~/.docker
 | 
			
		||||
        echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
 | 
			
		||||
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
        username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
    - name: Log in to the GitHub container registry
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
    - name: Run manifest
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${TAG}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          manifest
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								.github/workflows/matchers/pytest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/matchers/pytest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "problemMatcher": [
 | 
			
		||||
    {
 | 
			
		||||
      "owner": "pytest",
 | 
			
		||||
      "fileLocation": "absolute",
 | 
			
		||||
      "pattern": [
 | 
			
		||||
        {
 | 
			
		||||
          "regexp": "^\\s+File \"(.*)\", line (\\d+), in (.*)$",
 | 
			
		||||
          "file": 1,
 | 
			
		||||
          "line": 2
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "regexp": "^\\s+(.*)$",
 | 
			
		||||
          "message": 1
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										246
									
								
								.github/workflows/release-dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										246
									
								
								.github/workflows/release-dev.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,246 +0,0 @@
 | 
			
		||||
name: Publish dev releases to docker hub
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
    - dev
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml
 | 
			
		||||
 | 
			
		||||
  lint-clang-format:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-format
 | 
			
		||||
        run: script/clang-format -i
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
 | 
			
		||||
  lint-clang-tidy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        split: [1, 2, 3, 4]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }}
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
 | 
			
		||||
  lint-python:
 | 
			
		||||
    # Don't use the esphome-lint docker image because it may contain outdated requirements.
 | 
			
		||||
    # This way, all dependencies are cached via the cache action.
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/lint-python.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - name: Lint Custom
 | 
			
		||||
        run: script/ci-custom.py
 | 
			
		||||
      - name: Lint Python
 | 
			
		||||
        run: script/lint-python
 | 
			
		||||
      - name: Lint CODEOWNERS
 | 
			
		||||
        run: script/build_codeowners.py --check
 | 
			
		||||
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
          test:
 | 
			
		||||
          - test1
 | 
			
		||||
          - test2
 | 
			
		||||
          - test3
 | 
			
		||||
          - test4
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      # Use per test platformio cache because tests have different platform versions
 | 
			
		||||
      - name: Cache ~/.platformio
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            test-home-platformio-${{ matrix.test }}-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - run: esphome tests/${{ matrix.test }}.yaml compile
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - name: Install Github Actions annotator
 | 
			
		||||
        run: pip install pytest-github-actions-annotate-failures
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        run: |
 | 
			
		||||
          pytest \
 | 
			
		||||
            -qq \
 | 
			
		||||
            --durations=10 \
 | 
			
		||||
            -o console_output_style=count \
 | 
			
		||||
            tests
 | 
			
		||||
 | 
			
		||||
  deploy-docker:
 | 
			
		||||
    name: Build and publish docker containers
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest]
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly
 | 
			
		||||
        build_type: ["docker"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set TAG
 | 
			
		||||
        run: |
 | 
			
		||||
          TAG="${GITHUB_SHA:0:7}"
 | 
			
		||||
          echo "TAG=${TAG}" >> $GITHUB_ENV
 | 
			
		||||
      - name: Set up env variables
 | 
			
		||||
        run: |
 | 
			
		||||
          base_version="3.0.0"
 | 
			
		||||
 | 
			
		||||
          if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
 | 
			
		||||
            build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-hassio-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile.hassio"
 | 
			
		||||
          else
 | 
			
		||||
            build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV
 | 
			
		||||
          echo "BUILD_TO=${build_to}" >> $GITHUB_ENV
 | 
			
		||||
          echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV
 | 
			
		||||
      - name: Pull for cache
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull "${BUILD_TO}:dev" || true
 | 
			
		||||
      - name: Register QEMU binfmt
 | 
			
		||||
        run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker build \
 | 
			
		||||
            --build-arg "BUILD_FROM=${BUILD_FROM}" \
 | 
			
		||||
            --build-arg "BUILD_VERSION=${TAG}" \
 | 
			
		||||
            --tag "${BUILD_TO}:${TAG}" \
 | 
			
		||||
            --tag "${BUILD_TO}:dev" \
 | 
			
		||||
            --cache-from "${BUILD_TO}:dev" \
 | 
			
		||||
            --file "${DOCKERFILE}" \
 | 
			
		||||
            .
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_USER: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
        run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker push "${BUILD_TO}:${TAG}"
 | 
			
		||||
          docker push "${BUILD_TO}:dev"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  deploy-docker-manifest:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Enable experimental manifest support
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p ~/.docker
 | 
			
		||||
        echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        TAG="${GITHUB_SHA:0:7}"
 | 
			
		||||
        echo "TAG=${TAG}" >> $GITHUB_ENV
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_USER: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
 | 
			
		||||
    - name: "Create the manifest"
 | 
			
		||||
      run: |
 | 
			
		||||
        docker manifest create esphome/esphome:${TAG} \
 | 
			
		||||
          esphome/esphome-aarch64:${TAG} \
 | 
			
		||||
          esphome/esphome-amd64:${TAG} \
 | 
			
		||||
          esphome/esphome-armv7:${TAG}
 | 
			
		||||
        docker manifest push esphome/esphome:${TAG}
 | 
			
		||||
 | 
			
		||||
        docker manifest create esphome/esphome:dev \
 | 
			
		||||
          esphome/esphome-aarch64:${TAG} \
 | 
			
		||||
          esphome/esphome-amd64:${TAG} \
 | 
			
		||||
          esphome/esphome-armv7:${TAG}
 | 
			
		||||
        docker manifest push esphome/esphome:dev
 | 
			
		||||
							
								
								
									
										318
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										318
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,163 +1,35 @@
 | 
			
		||||
name: Publish Release
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  release:
 | 
			
		||||
    types: [published]
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: "0 2 * * *"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml
 | 
			
		||||
 | 
			
		||||
  lint-clang-format:
 | 
			
		||||
  init:
 | 
			
		||||
    name: Initialize build
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    outputs:
 | 
			
		||||
      tag: ${{ steps.tag.outputs.tag }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-format
 | 
			
		||||
        run: script/clang-format -i
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
 | 
			
		||||
  lint-clang-tidy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # cpp lint job runs with esphome-lint docker image so that clang-format-*
 | 
			
		||||
    # doesn't have to be installed
 | 
			
		||||
    container: esphome/esphome-lint:latest
 | 
			
		||||
    # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        split: [1, 2, 3, 4]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      # Set up the pio project so that the cpp checks know how files are compiled
 | 
			
		||||
      # (build flags, libraries etc)
 | 
			
		||||
      - name: Set up platformio environment
 | 
			
		||||
        run: pio init --ide atom
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
        id: tag
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }}
 | 
			
		||||
      - name: Suggest changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
 | 
			
		||||
  lint-python:
 | 
			
		||||
    # Don't use the esphome-lint docker image because it may contain outdated requirements.
 | 
			
		||||
    # This way, all dependencies are cached via the cache action.
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/lint-python.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - name: Lint Custom
 | 
			
		||||
        run: script/ci-custom.py
 | 
			
		||||
      - name: Lint Python
 | 
			
		||||
        run: script/lint-python
 | 
			
		||||
      - name: Lint CODEOWNERS
 | 
			
		||||
        run: script/build_codeowners.py --check
 | 
			
		||||
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
          test:
 | 
			
		||||
          - test1
 | 
			
		||||
          - test2
 | 
			
		||||
          - test3
 | 
			
		||||
          - test4
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      # Use per test platformio cache because tests have different platform versions
 | 
			
		||||
      - name: Cache ~/.platformio
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            test-home-platformio-${{ matrix.test }}-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - run: esphome tests/${{ matrix.test }}.yaml compile
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            esphome-pip-3.7-
 | 
			
		||||
      - name: Set up environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - name: Install Github Actions annotator
 | 
			
		||||
        run: pip install pytest-github-actions-annotate-failures
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        run: |
 | 
			
		||||
          pytest \
 | 
			
		||||
            -qq \
 | 
			
		||||
            --durations=10 \
 | 
			
		||||
            -o console_output_style=count \
 | 
			
		||||
            tests
 | 
			
		||||
          if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then
 | 
			
		||||
            TAG="${GITHUB_REF#refs/tags/}"
 | 
			
		||||
          else
 | 
			
		||||
            TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
 | 
			
		||||
            today="$(date --utc '+%Y%m%d')"
 | 
			
		||||
            TAG="${TAG}${today}"
 | 
			
		||||
          fi
 | 
			
		||||
          echo "::set-output name=tag::${TAG}"
 | 
			
		||||
 | 
			
		||||
  deploy-pypi:
 | 
			
		||||
    name: Build and publish to PyPi
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest]
 | 
			
		||||
    if: github.repository == 'esphome/esphome' && github.event_name == 'release'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
@@ -181,129 +53,95 @@ jobs:
 | 
			
		||||
    name: Build and publish docker containers
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest]
 | 
			
		||||
    needs: [init]
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        build_type: ["hassio", "docker"]
 | 
			
		||||
        build_type: ["ha-addon", "docker"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set TAG
 | 
			
		||||
        run: |
 | 
			
		||||
          TAG="${GITHUB_REF#refs/tags/v}"
 | 
			
		||||
          echo "TAG=${TAG}" >> $GITHUB_ENV
 | 
			
		||||
      - name: Set up env variables
 | 
			
		||||
        run: |
 | 
			
		||||
          base_version="3.0.0"
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
 | 
			
		||||
          if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
 | 
			
		||||
            build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-hassio-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile.hassio"
 | 
			
		||||
          else
 | 
			
		||||
            build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}"
 | 
			
		||||
            build_to="esphome/esphome-${{ matrix.arch }}"
 | 
			
		||||
            dockerfile="docker/Dockerfile"
 | 
			
		||||
          fi
 | 
			
		||||
    - name: Run build
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          build
 | 
			
		||||
 | 
			
		||||
          if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
 | 
			
		||||
            cache_tag="beta"
 | 
			
		||||
          else
 | 
			
		||||
            cache_tag="latest"
 | 
			
		||||
          fi
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
        username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
    - name: Log in to the GitHub container registry
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
          # Set env variables so these values don't need to be calculated again
 | 
			
		||||
          echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV
 | 
			
		||||
          echo "BUILD_TO=${build_to}" >> $GITHUB_ENV
 | 
			
		||||
          echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV
 | 
			
		||||
          echo "CACHE_TAG=${cache_tag}" >> $GITHUB_ENV
 | 
			
		||||
      - name: Pull for cache
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull "${BUILD_TO}:${CACHE_TAG}" || true
 | 
			
		||||
      - name: Register QEMU binfmt
 | 
			
		||||
        run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker build \
 | 
			
		||||
            --build-arg "BUILD_FROM=${BUILD_FROM}" \
 | 
			
		||||
            --build-arg "BUILD_VERSION=${TAG}" \
 | 
			
		||||
            --tag "${BUILD_TO}:${TAG}" \
 | 
			
		||||
            --cache-from "${BUILD_TO}:${CACHE_TAG}" \
 | 
			
		||||
            --file "${DOCKERFILE}" \
 | 
			
		||||
            .
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_USER: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
        run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
 | 
			
		||||
      - run: docker push "${BUILD_TO}:${TAG}"
 | 
			
		||||
 | 
			
		||||
      # Always publish to beta tag (also full releases)
 | 
			
		||||
      - name: Publish docker beta tag
 | 
			
		||||
        run: |
 | 
			
		||||
          docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta"
 | 
			
		||||
          docker push "${BUILD_TO}:beta"
 | 
			
		||||
 | 
			
		||||
      - if: ${{ !github.event.release.prerelease }}
 | 
			
		||||
        name: Publish docker latest tag
 | 
			
		||||
        run: |
 | 
			
		||||
          docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest"
 | 
			
		||||
          docker push "${BUILD_TO}:latest"
 | 
			
		||||
    - name: Run push
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          push
 | 
			
		||||
 | 
			
		||||
  deploy-docker-manifest:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    needs: [init, deploy-docker]
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        build_type: ["ha-addon", "docker"]
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
    - name: Enable experimental manifest support
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p ~/.docker
 | 
			
		||||
        echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        TAG="${GITHUB_REF#refs/tags/v}"
 | 
			
		||||
        echo "TAG=${TAG}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_USER: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
 | 
			
		||||
    - name: "Create the manifest"
 | 
			
		||||
      run: |
 | 
			
		||||
        docker manifest create esphome/esphome:${TAG} \
 | 
			
		||||
          esphome/esphome-aarch64:${TAG} \
 | 
			
		||||
          esphome/esphome-amd64:${TAG} \
 | 
			
		||||
          esphome/esphome-armv7:${TAG}
 | 
			
		||||
        docker manifest push esphome/esphome:${TAG}
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
        username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
        password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
    - name: Log in to the GitHub container registry
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
      with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
    - name: Publish docker beta tag
 | 
			
		||||
    - name: Run manifest
 | 
			
		||||
      run: |
 | 
			
		||||
        docker manifest create esphome/esphome:beta \
 | 
			
		||||
          esphome/esphome-aarch64:${TAG} \
 | 
			
		||||
          esphome/esphome-amd64:${TAG} \
 | 
			
		||||
          esphome/esphome-armv7:${TAG}
 | 
			
		||||
        docker manifest push esphome/esphome:beta
 | 
			
		||||
 | 
			
		||||
    - name: Publish docker latest tag
 | 
			
		||||
      if: ${{ !github.event.release.prerelease }}
 | 
			
		||||
      run: |
 | 
			
		||||
        docker manifest create esphome/esphome:latest \
 | 
			
		||||
          esphome/esphome-aarch64:${TAG} \
 | 
			
		||||
          esphome/esphome-amd64:${TAG} \
 | 
			
		||||
          esphome/esphome-armv7:${TAG}
 | 
			
		||||
        docker manifest push esphome/esphome:latest
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          manifest
 | 
			
		||||
 | 
			
		||||
  deploy-hassio-repo:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    if: github.repository == 'esphome/esphome' && github.event_name == 'release'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    steps:
 | 
			
		||||
      - env:
 | 
			
		||||
          TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }}
 | 
			
		||||
        run: |
 | 
			
		||||
          TAG="${GITHUB_REF#refs/tags/v}"
 | 
			
		||||
          TAG="${GITHUB_REF#refs/tags/}"
 | 
			
		||||
          curl \
 | 
			
		||||
            -u ":$TOKEN" \
 | 
			
		||||
            -X POST \
 | 
			
		||||
            -H "Accept: application/vnd.github.v3+json" \
 | 
			
		||||
            https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \
 | 
			
		||||
            -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}"
 | 
			
		||||
            -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -13,6 +13,9 @@ __pycache__/
 | 
			
		||||
# Intellij Idea
 | 
			
		||||
.idea
 | 
			
		||||
 | 
			
		||||
# Vim
 | 
			
		||||
*.swp
 | 
			
		||||
 | 
			
		||||
# Hide some OS X stuff
 | 
			
		||||
.DS_Store
 | 
			
		||||
.AppleDouble
 | 
			
		||||
@@ -100,6 +103,8 @@ CMakeLists.txt
 | 
			
		||||
 | 
			
		||||
# CMake
 | 
			
		||||
cmake-build-debug/
 | 
			
		||||
cmake-build-livingroom8266/
 | 
			
		||||
cmake-build-livingroom32/
 | 
			
		||||
cmake-build-release/
 | 
			
		||||
 | 
			
		||||
CMakeCache.txt
 | 
			
		||||
@@ -120,4 +125,5 @@ config/
 | 
			
		||||
tests/build/
 | 
			
		||||
tests/.esphome/
 | 
			
		||||
/.temp-clang-tidy.cpp
 | 
			
		||||
/.temp/
 | 
			
		||||
.pio/
 | 
			
		||||
 
 | 
			
		||||
@@ -23,5 +23,5 @@ repos:
 | 
			
		||||
      - id: no-commit-to-branch
 | 
			
		||||
        args:
 | 
			
		||||
          - --branch=dev
 | 
			
		||||
          - --branch=master
 | 
			
		||||
          - --branch=release
 | 
			
		||||
          - --branch=beta
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							@@ -1,11 +1,32 @@
 | 
			
		||||
{
 | 
			
		||||
    "version": "2.0.0",
 | 
			
		||||
    "tasks": [
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "tasks": [
 | 
			
		||||
    {
 | 
			
		||||
      "label": "run",
 | 
			
		||||
      "type": "shell",
 | 
			
		||||
      "command": "python3 -m esphome dashboard config/",
 | 
			
		||||
      "problemMatcher": []
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "label": "clang-tidy",
 | 
			
		||||
      "type": "shell",
 | 
			
		||||
      "command": "./script/clang-tidy",
 | 
			
		||||
      "problemMatcher": [
 | 
			
		||||
        {
 | 
			
		||||
            "label": "run",
 | 
			
		||||
            "type": "shell",
 | 
			
		||||
            "command": "python3 -m esphome config dashboard",
 | 
			
		||||
            "problemMatcher": []
 | 
			
		||||
          "owner": "clang-tidy",
 | 
			
		||||
          "fileLocation": "absolute",
 | 
			
		||||
          "pattern": [
 | 
			
		||||
            {
 | 
			
		||||
              "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$",
 | 
			
		||||
              "file": 1,
 | 
			
		||||
              "line": 2,
 | 
			
		||||
              "column": 3,
 | 
			
		||||
              "severity": 4,
 | 
			
		||||
              "message": 5
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -14,26 +14,39 @@ esphome/core/* @esphome/core
 | 
			
		||||
esphome/components/ac_dimmer/* @glmnet
 | 
			
		||||
esphome/components/adc/* @esphome/core
 | 
			
		||||
esphome/components/addressable_light/* @justfalter
 | 
			
		||||
esphome/components/airthings_ble/* @jeromelaban
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
esphome/components/anova/* @buxtronix
 | 
			
		||||
esphome/components/api/* @OttoWinter
 | 
			
		||||
esphome/components/async_tcp/* @OttoWinter
 | 
			
		||||
esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/b_parasite/* @rbaron
 | 
			
		||||
esphome/components/ballu/* @bazuchan
 | 
			
		||||
esphome/components/bang_bang/* @OttoWinter
 | 
			
		||||
esphome/components/binary_sensor/* @esphome/core
 | 
			
		||||
esphome/components/ble_client/* @buxtronix
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
esphome/components/canbus/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/captive_portal/* @OttoWinter
 | 
			
		||||
esphome/components/ccs811/* @habbie
 | 
			
		||||
esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
esphome/components/cs5460a/* @balrog-kun
 | 
			
		||||
esphome/components/ct_clamp/* @jesserockz
 | 
			
		||||
esphome/components/debug/* @OttoWinter
 | 
			
		||||
esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dht/* @OttoWinter
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/esp32_ble/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
			
		||||
esphome/components/esp32_improv/* @jesserockz
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
esphome/components/fastled_base/* @OttoWinter
 | 
			
		||||
@@ -41,8 +54,14 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh
 | 
			
		||||
esphome/components/globals/* @esphome/core
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter
 | 
			
		||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
 | 
			
		||||
esphome/components/i2c/* @esphome/core
 | 
			
		||||
esphome/components/improv/* @jesserockz
 | 
			
		||||
esphome/components/inkbird_ibsth1_mini/* @fkirill
 | 
			
		||||
esphome/components/inkplate6/* @jesserockz
 | 
			
		||||
esphome/components/integration/* @OttoWinter
 | 
			
		||||
@@ -61,31 +80,47 @@ esphome/components/mcp23x17_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23xxx_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp2515/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/mcp9808/* @k7hpn
 | 
			
		||||
esphome/components/midea_ac/* @dudanov
 | 
			
		||||
esphome/components/midea_dongle/* @dudanov
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
esphome/components/network/* @esphome/core
 | 
			
		||||
esphome/components/nextion/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/binary_sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/switch/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nfc/* @jesserockz
 | 
			
		||||
esphome/components/number/* @esphome/core
 | 
			
		||||
esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/pid/* @OttoWinter
 | 
			
		||||
esphome/components/pipsolar/* @andreashergert1984
 | 
			
		||||
esphome/components/pm1006/* @habbie
 | 
			
		||||
esphome/components/pmsa003i/* @sjtrny
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_spi/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/power_supply/* @esphome/core
 | 
			
		||||
esphome/components/pulse_meter/* @stevebaxter
 | 
			
		||||
esphome/components/pvvx_mithermometer/* @pasiz
 | 
			
		||||
esphome/components/rc522/* @glmnet
 | 
			
		||||
esphome/components/rc522_i2c/* @glmnet
 | 
			
		||||
esphome/components/rc522_spi/* @glmnet
 | 
			
		||||
esphome/components/restart/* @esphome/core
 | 
			
		||||
esphome/components/rf_bridge/* @jesserockz
 | 
			
		||||
esphome/components/rgbct/* @jesserockz
 | 
			
		||||
esphome/components/rtttl/* @glmnet
 | 
			
		||||
esphome/components/script/* @esphome/core
 | 
			
		||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
			
		||||
esphome/components/sdp3x/* @Azimath
 | 
			
		||||
esphome/components/selec_meter/* @sourabhjaiswal
 | 
			
		||||
esphome/components/select/* @esphome/core
 | 
			
		||||
esphome/components/sensor/* @esphome/core
 | 
			
		||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
			
		||||
esphome/components/sht4x/* @sjtrny
 | 
			
		||||
esphome/components/shutdown/* @esphome/core
 | 
			
		||||
esphome/components/sim800l/* @glmnet
 | 
			
		||||
esphome/components/sm2135/* @BoukeHaarsma23
 | 
			
		||||
esphome/components/socket/* @esphome/core
 | 
			
		||||
esphome/components/spi/* @esphome/core
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1322_spi/* @kbx81
 | 
			
		||||
@@ -100,17 +135,23 @@ esphome/components/ssd1351_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1351_spi/* @kbx81
 | 
			
		||||
esphome/components/st7735/* @SenexCrenshaw
 | 
			
		||||
esphome/components/st7789v/* @kbx81
 | 
			
		||||
esphome/components/st7920/* @marsjan155
 | 
			
		||||
esphome/components/substitutions/* @esphome/core
 | 
			
		||||
esphome/components/sun/* @OttoWinter
 | 
			
		||||
esphome/components/switch/* @esphome/core
 | 
			
		||||
esphome/components/t6615/* @tylermenezes
 | 
			
		||||
esphome/components/tca9548a/* @andreashergert1984
 | 
			
		||||
esphome/components/tcl112/* @glmnet
 | 
			
		||||
esphome/components/teleinfo/* @0hax
 | 
			
		||||
esphome/components/thermostat/* @kbx81
 | 
			
		||||
esphome/components/time/* @OttoWinter
 | 
			
		||||
esphome/components/tlc5947/* @rnauber
 | 
			
		||||
esphome/components/tm1637/* @glmnet
 | 
			
		||||
esphome/components/tmp102/* @timsavage
 | 
			
		||||
esphome/components/tmp117/* @Azimath
 | 
			
		||||
esphome/components/tof10120/* @wstrzalka
 | 
			
		||||
esphome/components/toshiba/* @kbx81
 | 
			
		||||
esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
esphome/components/tuya/binary_sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/climate/* @jesserockz
 | 
			
		||||
esphome/components/tuya/sensor/* @jesserockz
 | 
			
		||||
@@ -122,3 +163,4 @@ esphome/components/web_server_base/* @OttoWinter
 | 
			
		||||
esphome/components/whirlpool/* @glmnet
 | 
			
		||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
 | 
			
		||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
 | 
			
		||||
esphome/components/xpt2046/* @numo68
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
# ESPHome [](https://travis-ci.org/esphome/esphome) [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
 | 
			
		||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
 | 
			
		||||
 | 
			
		||||
[](https://esphome.io/)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-base-amd64:3.0.0
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-base:latest
 | 
			
		||||
FROM ${BUILD_FROM}
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Then copy esphome and install
 | 
			
		||||
@@ -27,4 +27,4 @@ WORKDIR /config
 | 
			
		||||
# in every docker command twice
 | 
			
		||||
ENTRYPOINT ["esphome"]
 | 
			
		||||
# When no arguments given, start the dashboard in the workdir
 | 
			
		||||
CMD ["/config", "dashboard"]
 | 
			
		||||
CMD ["dashboard", "/config"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1 @@
 | 
			
		||||
FROM esphome/esphome-base-amd64:3.0.0
 | 
			
		||||
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
RUN apt-get update \
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        python3-wheel \
 | 
			
		||||
        net-tools \
 | 
			
		||||
    && apt-get clean \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
WORKDIR /workspaces
 | 
			
		||||
ENV SHELL /bin/bash
 | 
			
		||||
FROM esphome/esphome-lint:1.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
ARG BUILD_FROM
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-hassio-base:latest
 | 
			
		||||
FROM ${BUILD_FROM}
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
FROM esphome/esphome-lint-base:3.0.0
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-lint-base:latest
 | 
			
		||||
FROM ${BUILD_FROM}
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt requirements_test.txt docker/platformio_install_deps.py  platformio.ini /
 | 
			
		||||
COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py  platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								docker/build.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										177
									
								
								docker/build.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,177 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import subprocess
 | 
			
		||||
import argparse
 | 
			
		||||
import platform
 | 
			
		||||
import shlex
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CHANNEL_DEV = 'dev'
 | 
			
		||||
CHANNEL_BETA = 'beta'
 | 
			
		||||
CHANNEL_RELEASE = 'release'
 | 
			
		||||
CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE]
 | 
			
		||||
 | 
			
		||||
ARCH_AMD64 = 'amd64'
 | 
			
		||||
ARCH_ARMV7 = 'armv7'
 | 
			
		||||
ARCH_AARCH64 = 'aarch64'
 | 
			
		||||
ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64]
 | 
			
		||||
 | 
			
		||||
TYPE_DOCKER = 'docker'
 | 
			
		||||
TYPE_HA_ADDON = 'ha-addon'
 | 
			
		||||
TYPE_LINT = 'lint'
 | 
			
		||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_VERSION = "4.2.0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
parser = argparse.ArgumentParser()
 | 
			
		||||
parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag")
 | 
			
		||||
parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for")
 | 
			
		||||
parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run")
 | 
			
		||||
parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them")
 | 
			
		||||
subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True)
 | 
			
		||||
build_parser = subparsers.add_parser("build", help="Build the image")
 | 
			
		||||
push_parser = subparsers.add_parser("push", help="Tag the already built image and push it to docker hub")
 | 
			
		||||
manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# only lists some possibilities, doesn't have to be perfect
 | 
			
		||||
# https://stackoverflow.com/a/45125525
 | 
			
		||||
UNAME_TO_ARCH = {
 | 
			
		||||
    "x86_64": ARCH_AMD64,
 | 
			
		||||
    "aarch64": ARCH_AARCH64,
 | 
			
		||||
    "aarch64_be": ARCH_AARCH64,
 | 
			
		||||
    "arm": ARCH_ARMV7,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass(frozen=True)
 | 
			
		||||
class DockerParams:
 | 
			
		||||
    build_from: str
 | 
			
		||||
    build_to: str
 | 
			
		||||
    manifest_to: str
 | 
			
		||||
    dockerfile: str
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def for_type_arch(cls, build_type, arch):
 | 
			
		||||
        prefix = {
 | 
			
		||||
            TYPE_DOCKER: "esphome/esphome",
 | 
			
		||||
            TYPE_HA_ADDON: "esphome/esphome-hassio",
 | 
			
		||||
            TYPE_LINT: "esphome/esphome-lint"
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        build_from = f"ghcr.io/{prefix}-base-{arch}:{BASE_VERSION}"
 | 
			
		||||
        build_to = f"{prefix}-{arch}"
 | 
			
		||||
        dockerfile = {
 | 
			
		||||
            TYPE_DOCKER: "docker/Dockerfile",
 | 
			
		||||
            TYPE_HA_ADDON: "docker/Dockerfile.hassio",
 | 
			
		||||
            TYPE_LINT: "docker/Dockerfile.lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        return cls(
 | 
			
		||||
            build_from=build_from,
 | 
			
		||||
            build_to=build_to,
 | 
			
		||||
            manifest_to=prefix,
 | 
			
		||||
            dockerfile=dockerfile
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    def run_command(*cmd, ignore_error: bool = False):
 | 
			
		||||
        print(f"$ {shlex.join(list(cmd))}")
 | 
			
		||||
        if not args.dry_run:
 | 
			
		||||
            rc = subprocess.call(list(cmd))
 | 
			
		||||
            if rc != 0 and not ignore_error:
 | 
			
		||||
                print("Command failed")
 | 
			
		||||
                sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    # detect channel from tag
 | 
			
		||||
    match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag)
 | 
			
		||||
    if match is None:
 | 
			
		||||
        channel = CHANNEL_DEV
 | 
			
		||||
    elif match.group(1) is None:
 | 
			
		||||
        channel = CHANNEL_RELEASE
 | 
			
		||||
    else:
 | 
			
		||||
        channel = CHANNEL_BETA
 | 
			
		||||
 | 
			
		||||
    tags_to_push = [args.tag]
 | 
			
		||||
    if channel == CHANNEL_DEV:
 | 
			
		||||
        tags_to_push.append("dev")
 | 
			
		||||
    elif channel == CHANNEL_BETA:
 | 
			
		||||
        tags_to_push.append("beta")
 | 
			
		||||
    elif channel == CHANNEL_RELEASE:
 | 
			
		||||
        # Additionally push to beta
 | 
			
		||||
        tags_to_push.append("beta")
 | 
			
		||||
        tags_to_push.append("latest")
 | 
			
		||||
 | 
			
		||||
    if args.command == "build":
 | 
			
		||||
        # 1. pull cache image
 | 
			
		||||
        params = DockerParams.for_type_arch(args.build_type, args.arch)
 | 
			
		||||
        cache_tag = {
 | 
			
		||||
            CHANNEL_DEV: "dev",
 | 
			
		||||
            CHANNEL_BETA: "beta",
 | 
			
		||||
            CHANNEL_RELEASE: "latest",
 | 
			
		||||
        }[channel]
 | 
			
		||||
        cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
 | 
			
		||||
        run_command("docker", "pull", cache_img, ignore_error=True)
 | 
			
		||||
 | 
			
		||||
        # 2. register QEMU binfmt (if not host arch)
 | 
			
		||||
        is_native = UNAME_TO_ARCH.get(platform.machine()) == args.arch
 | 
			
		||||
        if not is_native:
 | 
			
		||||
            run_command(
 | 
			
		||||
                "docker", "run", "--rm", "--privileged", "multiarch/qemu-user-static:5.2.0-2",
 | 
			
		||||
                "--reset", "-p", "yes"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # 3. build
 | 
			
		||||
        run_command(
 | 
			
		||||
            "docker", "build",
 | 
			
		||||
            "--build-arg", f"BUILD_FROM={params.build_from}",
 | 
			
		||||
            "--build-arg", f"BUILD_VERSION={args.tag}",
 | 
			
		||||
            "--tag", f"{params.build_to}:{args.tag}",
 | 
			
		||||
            "--cache-from", cache_img,
 | 
			
		||||
            "--file", params.dockerfile,
 | 
			
		||||
            "."
 | 
			
		||||
        )
 | 
			
		||||
    elif args.command == "push":
 | 
			
		||||
        params = DockerParams.for_type_arch(args.build_type, args.arch)
 | 
			
		||||
        imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
 | 
			
		||||
        imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
 | 
			
		||||
        src = imgs[0]
 | 
			
		||||
        # 1. tag images
 | 
			
		||||
        for img in imgs[1:]:
 | 
			
		||||
            run_command(
 | 
			
		||||
                "docker", "tag", src, img
 | 
			
		||||
            )
 | 
			
		||||
        # 2. push images
 | 
			
		||||
        for img in imgs:
 | 
			
		||||
            run_command(
 | 
			
		||||
                "docker", "push", img
 | 
			
		||||
            )
 | 
			
		||||
    elif args.command == "manifest":
 | 
			
		||||
        manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
 | 
			
		||||
 | 
			
		||||
        targets = [f"{manifest}:{tag}" for tag in tags_to_push]
 | 
			
		||||
        targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
 | 
			
		||||
        # 1. Create manifests
 | 
			
		||||
        for target in targets:
 | 
			
		||||
            cmd = ["docker", "manifest", "create", target]
 | 
			
		||||
            for arch in ARCHS:
 | 
			
		||||
                src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
 | 
			
		||||
                if target.startswith("ghcr.io"):
 | 
			
		||||
                    src = f"ghcr.io/{src}"
 | 
			
		||||
                cmd.append(src)
 | 
			
		||||
            run_command(*cmd)
 | 
			
		||||
        # 2. Push manifests
 | 
			
		||||
        for target in targets:
 | 
			
		||||
            run_command(
 | 
			
		||||
                "docker", "manifest", "push", target
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#!/usr/bin/with-contenv bashio
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
# Community Hass.io Add-ons: ESPHome
 | 
			
		||||
# This files installs the user ESPHome version if specified
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
 | 
			
		||||
declare esphome_version
 | 
			
		||||
 | 
			
		||||
if bashio::config.has_value 'esphome_version'; then
 | 
			
		||||
    esphome_version=$(bashio::config 'esphome_version')
 | 
			
		||||
    if [[ $esphome_version == *":"* ]]; then
 | 
			
		||||
      IFS=':' read -r -a array <<< "$esphome_version"
 | 
			
		||||
      username=${array[0]}
 | 
			
		||||
      ref=${array[1]}
 | 
			
		||||
    else
 | 
			
		||||
      username="esphome"
 | 
			
		||||
      ref=$esphome_version
 | 
			
		||||
    fi
 | 
			
		||||
    full_url="https://github.com/${username}/esphome/archive/${ref}.zip"
 | 
			
		||||
    bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..."
 | 
			
		||||
    pip3 install -U --no-cache-dir "${full_url}" \
 | 
			
		||||
      || bashio::exit.nok "Failed installing esphome pinned version."
 | 
			
		||||
fi
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#!/usr/bin/with-contenv bashio
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
# Community Hass.io Add-ons: ESPHome
 | 
			
		||||
# This files migrates the esphome config directory from the old path
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
 | 
			
		||||
if [[ ! -d /config/esphome && -d /config/esphomeyaml ]]; then
 | 
			
		||||
    echo "Moving config directory from /config/esphomeyaml to /config/esphome"
 | 
			
		||||
    mv /config/esphomeyaml /config/esphome
 | 
			
		||||
    mv /config/esphome/.esphomeyaml /config/esphome/.esphome
 | 
			
		||||
fi
 | 
			
		||||
@@ -23,4 +23,4 @@ if bashio::config.has_value 'relative_url'; then
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
bashio::log.info "Starting ESPHome dashboard..."
 | 
			
		||||
exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio
 | 
			
		||||
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ from esphome.config import iter_components, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
    CONF_BROKER,
 | 
			
		||||
    CONF_DEASSERT_RTS_DTR,
 | 
			
		||||
    CONF_LOGGER,
 | 
			
		||||
    CONF_OTA,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
@@ -18,7 +19,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_PLATFORMIO_OPTIONS,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.helpers import indent
 | 
			
		||||
from esphome.util import (
 | 
			
		||||
    run_external_command,
 | 
			
		||||
@@ -99,10 +100,21 @@ def run_miniterm(config, port):
 | 
			
		||||
    baud_rate = config["logger"][CONF_BAUD_RATE]
 | 
			
		||||
    if baud_rate == 0:
 | 
			
		||||
        _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.")
 | 
			
		||||
        return
 | 
			
		||||
    _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
 | 
			
		||||
 | 
			
		||||
    backtrace_state = False
 | 
			
		||||
    with serial.Serial(port, baudrate=baud_rate) as ser:
 | 
			
		||||
    ser = serial.Serial()
 | 
			
		||||
    ser.baudrate = baud_rate
 | 
			
		||||
    ser.port = port
 | 
			
		||||
 | 
			
		||||
    # We can't set to False by default since it leads to toggling and hence
 | 
			
		||||
    # ESP32 resets on some platforms.
 | 
			
		||||
    if config["logger"][CONF_DEASSERT_RTS_DTR]:
 | 
			
		||||
        ser.dtr = False
 | 
			
		||||
        ser.rts = False
 | 
			
		||||
 | 
			
		||||
    with ser:
 | 
			
		||||
        while True:
 | 
			
		||||
            try:
 | 
			
		||||
                raw = ser.readline()
 | 
			
		||||
@@ -127,15 +139,16 @@ def wrap_to_code(name, comp):
 | 
			
		||||
    coro = coroutine(comp.to_code)
 | 
			
		||||
 | 
			
		||||
    @functools.wraps(comp.to_code)
 | 
			
		||||
    @coroutine_with_priority(coro.priority)
 | 
			
		||||
    def wrapped(conf):
 | 
			
		||||
    async def wrapped(conf):
 | 
			
		||||
        cg.add(cg.LineComment(f"{name}:"))
 | 
			
		||||
        if comp.config_schema is not None:
 | 
			
		||||
            conf_str = yaml_util.dump(conf)
 | 
			
		||||
            conf_str = conf_str.replace("//", "")
 | 
			
		||||
            cg.add(cg.LineComment(indent(conf_str)))
 | 
			
		||||
        yield coro(conf)
 | 
			
		||||
        await coro(conf)
 | 
			
		||||
 | 
			
		||||
    if hasattr(coro, "priority"):
 | 
			
		||||
        wrapped.priority = coro.priority
 | 
			
		||||
    return wrapped
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -243,7 +256,7 @@ def show_logs(config, args, port):
 | 
			
		||||
        run_miniterm(config, port)
 | 
			
		||||
        return 0
 | 
			
		||||
    if get_port_type(port) == "NETWORK" and "api" in config:
 | 
			
		||||
        from esphome.api.client import run_logs
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
        return run_logs(config, port)
 | 
			
		||||
    if get_port_type(port) == "MQTT" and "mqtt" in config:
 | 
			
		||||
@@ -267,7 +280,7 @@ def clean_mqtt(config, args):
 | 
			
		||||
def command_wizard(args):
 | 
			
		||||
    from esphome import wizard
 | 
			
		||||
 | 
			
		||||
    return wizard.wizard(args.configuration[0])
 | 
			
		||||
    return wizard.wizard(args.configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_config(args, config):
 | 
			
		||||
@@ -283,7 +296,6 @@ def command_vscode(args):
 | 
			
		||||
 | 
			
		||||
    logging.disable(logging.INFO)
 | 
			
		||||
    logging.disable(logging.WARNING)
 | 
			
		||||
    CORE.config_path = args.configuration[0]
 | 
			
		||||
    vscode.read_config(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -303,7 +315,7 @@ def command_compile(args, config):
 | 
			
		||||
 | 
			
		||||
def command_upload(args, config):
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.upload_port,
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
@@ -318,7 +330,7 @@ def command_upload(args, config):
 | 
			
		||||
 | 
			
		||||
def command_logs(args, config):
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.serial_port,
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
@@ -336,7 +348,7 @@ def command_run(args, config):
 | 
			
		||||
        return exit_code
 | 
			
		||||
    _LOGGER.info("Successfully compiled program.")
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.upload_port,
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
@@ -349,7 +361,7 @@ def command_run(args, config):
 | 
			
		||||
    if args.no_logs:
 | 
			
		||||
        return 0
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.upload_port,
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=port,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
@@ -393,7 +405,7 @@ def command_update_all(args):
 | 
			
		||||
    import click
 | 
			
		||||
 | 
			
		||||
    success = {}
 | 
			
		||||
    files = list_yaml_files(args.configuration[0])
 | 
			
		||||
    files = list_yaml_files(args.configuration)
 | 
			
		||||
    twidth = 60
 | 
			
		||||
 | 
			
		||||
    def print_bar(middle_text):
 | 
			
		||||
@@ -407,7 +419,7 @@ def command_update_all(args):
 | 
			
		||||
        print("-" * twidth)
 | 
			
		||||
        print()
 | 
			
		||||
        rc = run_external_process(
 | 
			
		||||
            "esphome", "--dashboard", f, "run", "--no-logs", "--upload-port", "OTA"
 | 
			
		||||
            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
        )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f))
 | 
			
		||||
@@ -452,15 +464,17 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_args(argv):
 | 
			
		||||
    parser = argparse.ArgumentParser(description=f"ESPHome v{const.__version__}")
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-v", "--verbose", help="Enable verbose esphome logs.", action="store_true"
 | 
			
		||||
    options_parser = argparse.ArgumentParser(add_help=False)
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-q", "--quiet", help="Disable all esphome logs.", action="store_true"
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument("--dashboard", help=argparse.SUPPRESS, action="store_true")
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--dashboard", help=argparse.SUPPRESS, action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "-s",
 | 
			
		||||
        "--substitution",
 | 
			
		||||
        nargs=2,
 | 
			
		||||
@@ -468,17 +482,35 @@ def parse_args(argv):
 | 
			
		||||
        help="Add a substitution",
 | 
			
		||||
        metavar=("key", "value"),
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file.", nargs="*"
 | 
			
		||||
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description=f"ESPHome v{const.__version__}", parents=[options_parser]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    subparsers = parser.add_subparsers(help="Commands", dest="command")
 | 
			
		||||
    mqtt_options = argparse.ArgumentParser(add_help=False)
 | 
			
		||||
    mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
 | 
			
		||||
    mqtt_options.add_argument("--username", help="Manually set the MQTT username.")
 | 
			
		||||
    mqtt_options.add_argument("--password", help="Manually set the MQTT password.")
 | 
			
		||||
    mqtt_options.add_argument("--client-id", help="Manually set the MQTT client id.")
 | 
			
		||||
 | 
			
		||||
    subparsers = parser.add_subparsers(
 | 
			
		||||
        help="Command to run:", dest="command", metavar="command"
 | 
			
		||||
    )
 | 
			
		||||
    subparsers.required = True
 | 
			
		||||
    subparsers.add_parser("config", help="Validate the configuration and spit it out.")
 | 
			
		||||
 | 
			
		||||
    parser_config = subparsers.add_parser(
 | 
			
		||||
        "config", help="Validate the configuration and spit it out."
 | 
			
		||||
    )
 | 
			
		||||
    parser_config.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_compile = subparsers.add_parser(
 | 
			
		||||
        "compile", help="Read the configuration and compile a program."
 | 
			
		||||
    )
 | 
			
		||||
    parser_compile.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
    parser_compile.add_argument(
 | 
			
		||||
        "--only-generate",
 | 
			
		||||
        help="Only generate source code, do not compile.",
 | 
			
		||||
@@ -486,108 +518,200 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_upload = subparsers.add_parser(
 | 
			
		||||
        "upload", help="Validate the configuration " "and upload the latest binary."
 | 
			
		||||
        "upload", help="Validate the configuration and upload the latest binary."
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--upload-port",
 | 
			
		||||
        help="Manually specify the upload port to use. "
 | 
			
		||||
        "For example /dev/cu.SLAB_USBtoUART.",
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_logs = subparsers.add_parser(
 | 
			
		||||
        "logs", help="Validate the configuration " "and show all MQTT logs."
 | 
			
		||||
        "logs",
 | 
			
		||||
        help="Validate the configuration and show all logs.",
 | 
			
		||||
        parents=[mqtt_options],
 | 
			
		||||
    )
 | 
			
		||||
    parser_logs.add_argument("--topic", help="Manually set the topic to subscribe to.")
 | 
			
		||||
    parser_logs.add_argument("--username", help="Manually set the username.")
 | 
			
		||||
    parser_logs.add_argument("--password", help="Manually set the password.")
 | 
			
		||||
    parser_logs.add_argument("--client-id", help="Manually set the client id.")
 | 
			
		||||
    parser_logs.add_argument(
 | 
			
		||||
        "--serial-port",
 | 
			
		||||
        help="Manually specify a serial port to use"
 | 
			
		||||
        "For example /dev/cu.SLAB_USBtoUART.",
 | 
			
		||||
        "configuration", help="Your YAML configuration file.", nargs=1
 | 
			
		||||
    )
 | 
			
		||||
    parser_logs.add_argument(
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_run = subparsers.add_parser(
 | 
			
		||||
        "run",
 | 
			
		||||
        help="Validate the configuration, create a binary, "
 | 
			
		||||
        "upload it, and start MQTT logs.",
 | 
			
		||||
        help="Validate the configuration, create a binary, upload it, and start logs.",
 | 
			
		||||
        parents=[mqtt_options],
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--upload-port",
 | 
			
		||||
        help="Manually specify the upload port/ip to use. "
 | 
			
		||||
        "For example /dev/cu.SLAB_USBtoUART.",
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--no-logs", help="Disable starting MQTT logs.", action="store_true"
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--topic", help="Manually set the topic to subscribe to for logs."
 | 
			
		||||
        "--no-logs", help="Disable starting logs.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--username", help="Manually set the MQTT username for logs."
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--password", help="Manually set the MQTT password for logs."
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument("--client-id", help="Manually set the client id for logs.")
 | 
			
		||||
 | 
			
		||||
    parser_clean = subparsers.add_parser(
 | 
			
		||||
        "clean-mqtt", help="Helper to clear an MQTT topic from " "retain messages."
 | 
			
		||||
        "clean-mqtt",
 | 
			
		||||
        help="Helper to clear retained messages from an MQTT topic.",
 | 
			
		||||
        parents=[mqtt_options],
 | 
			
		||||
    )
 | 
			
		||||
    parser_clean.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
    parser_clean.add_argument("--topic", help="Manually set the topic to subscribe to.")
 | 
			
		||||
    parser_clean.add_argument("--username", help="Manually set the username.")
 | 
			
		||||
    parser_clean.add_argument("--password", help="Manually set the password.")
 | 
			
		||||
    parser_clean.add_argument("--client-id", help="Manually set the client id.")
 | 
			
		||||
 | 
			
		||||
    subparsers.add_parser(
 | 
			
		||||
    parser_wizard = subparsers.add_parser(
 | 
			
		||||
        "wizard",
 | 
			
		||||
        help="A helpful setup wizard that will guide "
 | 
			
		||||
        "you through setting up esphome.",
 | 
			
		||||
        help="A helpful setup wizard that will guide you through setting up ESPHome.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_wizard.add_argument(
 | 
			
		||||
        "configuration",
 | 
			
		||||
        help="Your YAML configuration file.",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    subparsers.add_parser(
 | 
			
		||||
    parser_fingerprint = subparsers.add_parser(
 | 
			
		||||
        "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker."
 | 
			
		||||
    )
 | 
			
		||||
    parser_fingerprint.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    subparsers.add_parser("version", help="Print the esphome version and exit.")
 | 
			
		||||
    subparsers.add_parser("version", help="Print the ESPHome version and exit.")
 | 
			
		||||
 | 
			
		||||
    subparsers.add_parser("clean", help="Delete all temporary build files.")
 | 
			
		||||
    parser_clean = subparsers.add_parser(
 | 
			
		||||
        "clean", help="Delete all temporary build files."
 | 
			
		||||
    )
 | 
			
		||||
    parser_clean.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    dashboard = subparsers.add_parser(
 | 
			
		||||
    parser_dashboard = subparsers.add_parser(
 | 
			
		||||
        "dashboard", help="Create a simple web server for a dashboard."
 | 
			
		||||
    )
 | 
			
		||||
    dashboard.add_argument(
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "configuration",
 | 
			
		||||
        help="Your YAML configuration file directory.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--port",
 | 
			
		||||
        help="The HTTP port to open connections on. Defaults to 6052.",
 | 
			
		||||
        type=int,
 | 
			
		||||
        default=6052,
 | 
			
		||||
    )
 | 
			
		||||
    dashboard.add_argument(
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--username",
 | 
			
		||||
        help="The optional username to require " "for authentication.",
 | 
			
		||||
        help="The optional username to require for authentication.",
 | 
			
		||||
        type=str,
 | 
			
		||||
        default="",
 | 
			
		||||
    )
 | 
			
		||||
    dashboard.add_argument(
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--password",
 | 
			
		||||
        help="The optional password to require " "for authentication.",
 | 
			
		||||
        help="The optional password to require for authentication.",
 | 
			
		||||
        type=str,
 | 
			
		||||
        default="",
 | 
			
		||||
    )
 | 
			
		||||
    dashboard.add_argument(
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--open-ui", help="Open the dashboard UI in a browser.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    dashboard.add_argument("--hassio", help=argparse.SUPPRESS, action="store_true")
 | 
			
		||||
    dashboard.add_argument(
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--hassio", help=argparse.SUPPRESS, action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    parser_dashboard.add_argument(
 | 
			
		||||
        "--socket", help="Make the dashboard serve under a unix socket", type=str
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    vscode = subparsers.add_parser("vscode", help=argparse.SUPPRESS)
 | 
			
		||||
    vscode.add_argument("--ace", action="store_true")
 | 
			
		||||
    parser_vscode = subparsers.add_parser("vscode")
 | 
			
		||||
    parser_vscode.add_argument("configuration", help="Your YAML configuration file.")
 | 
			
		||||
    parser_vscode.add_argument("--ace", action="store_true")
 | 
			
		||||
 | 
			
		||||
    subparsers.add_parser("update-all", help=argparse.SUPPRESS)
 | 
			
		||||
    parser_update = subparsers.add_parser("update-all")
 | 
			
		||||
    parser_update.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file directories.", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return parser.parse_args(argv[1:])
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
    # Unfortunately this can't be done by adding another configuration argument to the
 | 
			
		||||
    # main config parser, as argparse is greedy when parsing arguments, so in regular
 | 
			
		||||
    # usage it'll eat the command as the configuration argument and error out out
 | 
			
		||||
    # because it can't parse the configuration as a command.
 | 
			
		||||
    #
 | 
			
		||||
    # Instead, if parsing using the current format fails, construct an ad-hoc parser
 | 
			
		||||
    # that doesn't actually process the arguments, but parses them enough to let us
 | 
			
		||||
    # figure out if the old format is used. In that case, swap the command and
 | 
			
		||||
    # configuration in the arguments and retry with the normal parser (and raise
 | 
			
		||||
    # a deprecation warning).
 | 
			
		||||
    arguments = argv[1:]
 | 
			
		||||
 | 
			
		||||
    # On Python 3.9+ we can simply set exit_on_error=False in the constructor
 | 
			
		||||
    def _raise(x):
 | 
			
		||||
        raise argparse.ArgumentError(None, x)
 | 
			
		||||
 | 
			
		||||
    # First, try new-style parsing, but don't exit in case of failure
 | 
			
		||||
    try:
 | 
			
		||||
        # duplicate parser so that we can use the original one to raise errors later on
 | 
			
		||||
        current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
 | 
			
		||||
        current_parser.set_defaults(deprecated_argv_suggestion=None)
 | 
			
		||||
        current_parser.error = _raise
 | 
			
		||||
        return current_parser.parse_args(arguments)
 | 
			
		||||
    except argparse.ArgumentError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # Second, try compat parsing and rearrange the command-line if it succeeds
 | 
			
		||||
    # Disable argparse's built-in help option and add it manually to prevent this
 | 
			
		||||
    # parser from printing the help messagefor the old format when invoked with -h.
 | 
			
		||||
    compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
 | 
			
		||||
    compat_parser.add_argument("-h", "--help", action="store_true")
 | 
			
		||||
    compat_parser.add_argument("configuration", nargs="*")
 | 
			
		||||
    compat_parser.add_argument(
 | 
			
		||||
        "command",
 | 
			
		||||
        choices=[
 | 
			
		||||
            "config",
 | 
			
		||||
            "compile",
 | 
			
		||||
            "upload",
 | 
			
		||||
            "logs",
 | 
			
		||||
            "run",
 | 
			
		||||
            "clean-mqtt",
 | 
			
		||||
            "wizard",
 | 
			
		||||
            "mqtt-fingerprint",
 | 
			
		||||
            "version",
 | 
			
		||||
            "clean",
 | 
			
		||||
            "dashboard",
 | 
			
		||||
            "vscode",
 | 
			
		||||
            "update-all",
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        compat_parser.error = _raise
 | 
			
		||||
        result, unparsed = compat_parser.parse_known_args(argv[1:])
 | 
			
		||||
        last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
 | 
			
		||||
        unparsed = [
 | 
			
		||||
            "--device" if arg in ("--upload-port", "--serial-port") else arg
 | 
			
		||||
            for arg in unparsed
 | 
			
		||||
        ]
 | 
			
		||||
        arguments = (
 | 
			
		||||
            arguments[0:last_option]
 | 
			
		||||
            + [result.command]
 | 
			
		||||
            + result.configuration
 | 
			
		||||
            + unparsed
 | 
			
		||||
        )
 | 
			
		||||
        deprecated_argv_suggestion = arguments
 | 
			
		||||
    except argparse.ArgumentError:
 | 
			
		||||
        # old-style parsing failed, don't suggest any argument
 | 
			
		||||
        deprecated_argv_suggestion = None
 | 
			
		||||
 | 
			
		||||
    # Finally, run the new-style parser again with the possibly swapped arguments,
 | 
			
		||||
    # and let it error out if the command is unparsable.
 | 
			
		||||
    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
 | 
			
		||||
    return parser.parse_args(arguments)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_esphome(argv):
 | 
			
		||||
@@ -595,9 +719,13 @@ def run_esphome(argv):
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
 | 
			
		||||
    setup_log(args.verbose, args.quiet)
 | 
			
		||||
    if args.command != "version" and not args.configuration:
 | 
			
		||||
        _LOGGER.error("Missing configuration parameter, see esphome --help.")
 | 
			
		||||
        return 1
 | 
			
		||||
    if args.deprecated_argv_suggestion is not None and args.command != "vscode":
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "Calling ESPHome with the configuration before the command is deprecated "
 | 
			
		||||
            "and will be removed in the future. "
 | 
			
		||||
        )
 | 
			
		||||
        _LOGGER.warning("Please instead use:")
 | 
			
		||||
        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion))
 | 
			
		||||
 | 
			
		||||
    if sys.version_info < (3, 7, 0):
 | 
			
		||||
        _LOGGER.error(
 | 
			
		||||
@@ -610,7 +738,7 @@ def run_esphome(argv):
 | 
			
		||||
        try:
 | 
			
		||||
            return PRE_CONFIG_ACTIONS[args.command](args)
 | 
			
		||||
        except EsphomeError as e:
 | 
			
		||||
            _LOGGER.error(e)
 | 
			
		||||
            _LOGGER.error(e, exc_info=args.verbose)
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
    for conf_path in args.configuration:
 | 
			
		||||
@@ -628,7 +756,7 @@ def run_esphome(argv):
 | 
			
		||||
        try:
 | 
			
		||||
            rc = POST_CONFIG_ACTIONS[args.command](args, config)
 | 
			
		||||
        except EsphomeError as e:
 | 
			
		||||
            _LOGGER.error(e)
 | 
			
		||||
            _LOGGER.error(e, exc_info=args.verbose)
 | 
			
		||||
            return 1
 | 
			
		||||
        if rc != 0:
 | 
			
		||||
            return rc
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,518 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import socket
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-import
 | 
			
		||||
from typing import Optional  # noqa
 | 
			
		||||
from google.protobuf import message  # noqa
 | 
			
		||||
 | 
			
		||||
from esphome import const
 | 
			
		||||
import esphome.api.api_pb2 as pb
 | 
			
		||||
from esphome.const import CONF_PASSWORD, CONF_PORT
 | 
			
		||||
from esphome.core import EsphomeError
 | 
			
		||||
from esphome.helpers import resolve_ip_address, indent
 | 
			
		||||
from esphome.log import color, Fore
 | 
			
		||||
from esphome.util import safe_print
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIConnectionError(EsphomeError):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MESSAGE_TYPE_TO_PROTO = {
 | 
			
		||||
    1: pb.HelloRequest,
 | 
			
		||||
    2: pb.HelloResponse,
 | 
			
		||||
    3: pb.ConnectRequest,
 | 
			
		||||
    4: pb.ConnectResponse,
 | 
			
		||||
    5: pb.DisconnectRequest,
 | 
			
		||||
    6: pb.DisconnectResponse,
 | 
			
		||||
    7: pb.PingRequest,
 | 
			
		||||
    8: pb.PingResponse,
 | 
			
		||||
    9: pb.DeviceInfoRequest,
 | 
			
		||||
    10: pb.DeviceInfoResponse,
 | 
			
		||||
    11: pb.ListEntitiesRequest,
 | 
			
		||||
    12: pb.ListEntitiesBinarySensorResponse,
 | 
			
		||||
    13: pb.ListEntitiesCoverResponse,
 | 
			
		||||
    14: pb.ListEntitiesFanResponse,
 | 
			
		||||
    15: pb.ListEntitiesLightResponse,
 | 
			
		||||
    16: pb.ListEntitiesSensorResponse,
 | 
			
		||||
    17: pb.ListEntitiesSwitchResponse,
 | 
			
		||||
    18: pb.ListEntitiesTextSensorResponse,
 | 
			
		||||
    19: pb.ListEntitiesDoneResponse,
 | 
			
		||||
    20: pb.SubscribeStatesRequest,
 | 
			
		||||
    21: pb.BinarySensorStateResponse,
 | 
			
		||||
    22: pb.CoverStateResponse,
 | 
			
		||||
    23: pb.FanStateResponse,
 | 
			
		||||
    24: pb.LightStateResponse,
 | 
			
		||||
    25: pb.SensorStateResponse,
 | 
			
		||||
    26: pb.SwitchStateResponse,
 | 
			
		||||
    27: pb.TextSensorStateResponse,
 | 
			
		||||
    28: pb.SubscribeLogsRequest,
 | 
			
		||||
    29: pb.SubscribeLogsResponse,
 | 
			
		||||
    30: pb.CoverCommandRequest,
 | 
			
		||||
    31: pb.FanCommandRequest,
 | 
			
		||||
    32: pb.LightCommandRequest,
 | 
			
		||||
    33: pb.SwitchCommandRequest,
 | 
			
		||||
    34: pb.SubscribeServiceCallsRequest,
 | 
			
		||||
    35: pb.ServiceCallResponse,
 | 
			
		||||
    36: pb.GetTimeRequest,
 | 
			
		||||
    37: pb.GetTimeResponse,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _varuint_to_bytes(value):
 | 
			
		||||
    if value <= 0x7F:
 | 
			
		||||
        return bytes([value])
 | 
			
		||||
 | 
			
		||||
    ret = bytes()
 | 
			
		||||
    while value:
 | 
			
		||||
        temp = value & 0x7F
 | 
			
		||||
        value >>= 7
 | 
			
		||||
        if value:
 | 
			
		||||
            ret += bytes([temp | 0x80])
 | 
			
		||||
        else:
 | 
			
		||||
            ret += bytes([temp])
 | 
			
		||||
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _bytes_to_varuint(value):
 | 
			
		||||
    result = 0
 | 
			
		||||
    bitpos = 0
 | 
			
		||||
    for val in value:
 | 
			
		||||
        result |= (val & 0x7F) << bitpos
 | 
			
		||||
        bitpos += 7
 | 
			
		||||
        if (val & 0x80) == 0:
 | 
			
		||||
            return result
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=too-many-instance-attributes,not-callable
 | 
			
		||||
class APIClient(threading.Thread):
 | 
			
		||||
    def __init__(self, address, port, password):
 | 
			
		||||
        threading.Thread.__init__(self)
 | 
			
		||||
        self._address = address  # type: str
 | 
			
		||||
        self._port = port  # type: int
 | 
			
		||||
        self._password = password  # type: Optional[str]
 | 
			
		||||
        self._socket = None  # type: Optional[socket.socket]
 | 
			
		||||
        self._socket_open_event = threading.Event()
 | 
			
		||||
        self._socket_write_lock = threading.Lock()
 | 
			
		||||
        self._connected = False
 | 
			
		||||
        self._authenticated = False
 | 
			
		||||
        self._message_handlers = []
 | 
			
		||||
        self._keepalive = 5
 | 
			
		||||
        self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
        self.on_disconnect = None
 | 
			
		||||
        self.on_connect = None
 | 
			
		||||
        self.on_login = None
 | 
			
		||||
        self.auto_reconnect = False
 | 
			
		||||
        self._running_event = threading.Event()
 | 
			
		||||
        self._stop_event = threading.Event()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def stopped(self):
 | 
			
		||||
        return self._stop_event.is_set()
 | 
			
		||||
 | 
			
		||||
    def _refresh_ping(self):
 | 
			
		||||
        if self._ping_timer is not None:
 | 
			
		||||
            self._ping_timer.cancel()
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
        def func():
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
            if self._connected:
 | 
			
		||||
                try:
 | 
			
		||||
                    self.ping()
 | 
			
		||||
                except APIConnectionError as err:
 | 
			
		||||
                    self._fatal_error(err)
 | 
			
		||||
                else:
 | 
			
		||||
                    self._refresh_ping()
 | 
			
		||||
 | 
			
		||||
        self._ping_timer = threading.Timer(self._keepalive, func)
 | 
			
		||||
        self._ping_timer.start()
 | 
			
		||||
 | 
			
		||||
    def _cancel_ping(self):
 | 
			
		||||
        if self._ping_timer is not None:
 | 
			
		||||
            self._ping_timer.cancel()
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
    def _close_socket(self):
 | 
			
		||||
        self._cancel_ping()
 | 
			
		||||
        if self._socket is not None:
 | 
			
		||||
            self._socket.close()
 | 
			
		||||
            self._socket = None
 | 
			
		||||
        self._socket_open_event.clear()
 | 
			
		||||
        self._connected = False
 | 
			
		||||
        self._authenticated = False
 | 
			
		||||
        self._message_handlers = []
 | 
			
		||||
 | 
			
		||||
    def stop(self, force=False):
 | 
			
		||||
        if self.stopped:
 | 
			
		||||
            raise ValueError
 | 
			
		||||
 | 
			
		||||
        if self._connected and not force:
 | 
			
		||||
            try:
 | 
			
		||||
                self.disconnect()
 | 
			
		||||
            except APIConnectionError:
 | 
			
		||||
                pass
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        self._stop_event.set()
 | 
			
		||||
        if not force:
 | 
			
		||||
            self.join()
 | 
			
		||||
 | 
			
		||||
    def connect(self):
 | 
			
		||||
        if not self._running_event.wait(0.1):
 | 
			
		||||
            raise APIConnectionError("You need to call start() first!")
 | 
			
		||||
 | 
			
		||||
        if self._connected:
 | 
			
		||||
            self.disconnect(on_disconnect=False)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            ip = resolve_ip_address(self._address)
 | 
			
		||||
        except EsphomeError as err:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Error resolving IP address of %s. Is it connected to WiFi?",
 | 
			
		||||
                self._address,
 | 
			
		||||
            )
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "(If this error persists, please set a static IP address: "
 | 
			
		||||
                "https://esphome.io/components/wifi.html#manual-ips)"
 | 
			
		||||
            )
 | 
			
		||||
            raise APIConnectionError(err) from err
 | 
			
		||||
 | 
			
		||||
        _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
 | 
			
		||||
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
			
		||||
        self._socket.settimeout(10.0)
 | 
			
		||||
        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 | 
			
		||||
        try:
 | 
			
		||||
            self._socket.connect((ip, self._port))
 | 
			
		||||
        except OSError as err:
 | 
			
		||||
            err = APIConnectionError(f"Error connecting to {ip}: {err}")
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
        self._socket.settimeout(0.1)
 | 
			
		||||
 | 
			
		||||
        self._socket_open_event.set()
 | 
			
		||||
 | 
			
		||||
        hello = pb.HelloRequest()
 | 
			
		||||
        hello.client_info = f"ESPHome v{const.__version__}"
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self._send_message_await_response(hello, pb.HelloResponse)
 | 
			
		||||
        except APIConnectionError as err:
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
        _LOGGER.debug(
 | 
			
		||||
            "Successfully connected to %s ('%s' API=%s.%s)",
 | 
			
		||||
            self._address,
 | 
			
		||||
            resp.server_info,
 | 
			
		||||
            resp.api_version_major,
 | 
			
		||||
            resp.api_version_minor,
 | 
			
		||||
        )
 | 
			
		||||
        self._connected = True
 | 
			
		||||
        self._refresh_ping()
 | 
			
		||||
        if self.on_connect is not None:
 | 
			
		||||
            self.on_connect()
 | 
			
		||||
 | 
			
		||||
    def _check_connected(self):
 | 
			
		||||
        if not self._connected:
 | 
			
		||||
            err = APIConnectionError("Must be connected!")
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
 | 
			
		||||
    def login(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        if self._authenticated:
 | 
			
		||||
            raise APIConnectionError("Already logged in!")
 | 
			
		||||
 | 
			
		||||
        connect = pb.ConnectRequest()
 | 
			
		||||
        if self._password is not None:
 | 
			
		||||
            connect.password = self._password
 | 
			
		||||
        resp = self._send_message_await_response(connect, pb.ConnectResponse)
 | 
			
		||||
        if resp.invalid_password:
 | 
			
		||||
            raise APIConnectionError("Invalid password!")
 | 
			
		||||
 | 
			
		||||
        self._authenticated = True
 | 
			
		||||
        if self.on_login is not None:
 | 
			
		||||
            self.on_login()
 | 
			
		||||
 | 
			
		||||
    def _fatal_error(self, err):
 | 
			
		||||
        was_connected = self._connected
 | 
			
		||||
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        if was_connected and self.on_disconnect is not None:
 | 
			
		||||
            self.on_disconnect(err)
 | 
			
		||||
 | 
			
		||||
    def _write(self, data):  # type: (bytes) -> None
 | 
			
		||||
        if self._socket is None:
 | 
			
		||||
            raise APIConnectionError("Socket closed")
 | 
			
		||||
 | 
			
		||||
        # _LOGGER.debug("Write: %s", format_bytes(data))
 | 
			
		||||
        with self._socket_write_lock:
 | 
			
		||||
            try:
 | 
			
		||||
                self._socket.sendall(data)
 | 
			
		||||
            except OSError as err:
 | 
			
		||||
                err = APIConnectionError(f"Error while writing data: {err}")
 | 
			
		||||
                self._fatal_error(err)
 | 
			
		||||
                raise err
 | 
			
		||||
 | 
			
		||||
    def _send_message(self, msg):
 | 
			
		||||
        # type: (message.Message) -> None
 | 
			
		||||
        for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
 | 
			
		||||
            if isinstance(msg, klass):
 | 
			
		||||
                break
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError
 | 
			
		||||
 | 
			
		||||
        encoded = msg.SerializeToString()
 | 
			
		||||
        _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg)))
 | 
			
		||||
        req = bytes([0])
 | 
			
		||||
        req += _varuint_to_bytes(len(encoded))
 | 
			
		||||
        req += _varuint_to_bytes(message_type)
 | 
			
		||||
        req += encoded
 | 
			
		||||
        self._write(req)
 | 
			
		||||
 | 
			
		||||
    def _send_message_await_response_complex(
 | 
			
		||||
        self, send_msg, do_append, do_stop, timeout=5
 | 
			
		||||
    ):
 | 
			
		||||
        event = threading.Event()
 | 
			
		||||
        responses = []
 | 
			
		||||
 | 
			
		||||
        def on_message(resp):
 | 
			
		||||
            if do_append(resp):
 | 
			
		||||
                responses.append(resp)
 | 
			
		||||
            if do_stop(resp):
 | 
			
		||||
                event.set()
 | 
			
		||||
 | 
			
		||||
        self._message_handlers.append(on_message)
 | 
			
		||||
        self._send_message(send_msg)
 | 
			
		||||
        ret = event.wait(timeout)
 | 
			
		||||
        try:
 | 
			
		||||
            self._message_handlers.remove(on_message)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        if not ret:
 | 
			
		||||
            raise APIConnectionError("Timeout while waiting for message response!")
 | 
			
		||||
        return responses
 | 
			
		||||
 | 
			
		||||
    def _send_message_await_response(self, send_msg, response_type, timeout=5):
 | 
			
		||||
        def is_response(msg):
 | 
			
		||||
            return isinstance(msg, response_type)
 | 
			
		||||
 | 
			
		||||
        return self._send_message_await_response_complex(
 | 
			
		||||
            send_msg, is_response, is_response, timeout
 | 
			
		||||
        )[0]
 | 
			
		||||
 | 
			
		||||
    def device_info(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        return self._send_message_await_response(
 | 
			
		||||
            pb.DeviceInfoRequest(), pb.DeviceInfoResponse
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def ping(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
 | 
			
		||||
 | 
			
		||||
    def disconnect(self, on_disconnect=True):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self._send_message_await_response(
 | 
			
		||||
                pb.DisconnectRequest(), pb.DisconnectResponse
 | 
			
		||||
            )
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            pass
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        if self.on_disconnect is not None and on_disconnect:
 | 
			
		||||
            self.on_disconnect(None)
 | 
			
		||||
 | 
			
		||||
    def _check_authenticated(self):
 | 
			
		||||
        if not self._authenticated:
 | 
			
		||||
            raise APIConnectionError("Must login first!")
 | 
			
		||||
 | 
			
		||||
    def subscribe_logs(self, on_log, log_level=7, dump_config=False):
 | 
			
		||||
        self._check_authenticated()
 | 
			
		||||
 | 
			
		||||
        def on_msg(msg):
 | 
			
		||||
            if isinstance(msg, pb.SubscribeLogsResponse):
 | 
			
		||||
                on_log(msg)
 | 
			
		||||
 | 
			
		||||
        self._message_handlers.append(on_msg)
 | 
			
		||||
        req = pb.SubscribeLogsRequest(dump_config=dump_config)
 | 
			
		||||
        req.level = log_level
 | 
			
		||||
        self._send_message(req)
 | 
			
		||||
 | 
			
		||||
    def _recv(self, amount):
 | 
			
		||||
        ret = bytes()
 | 
			
		||||
        if amount == 0:
 | 
			
		||||
            return ret
 | 
			
		||||
 | 
			
		||||
        while len(ret) < amount:
 | 
			
		||||
            if self.stopped:
 | 
			
		||||
                raise APIConnectionError("Stopped!")
 | 
			
		||||
            if not self._socket_open_event.is_set():
 | 
			
		||||
                raise APIConnectionError("No socket!")
 | 
			
		||||
            try:
 | 
			
		||||
                val = self._socket.recv(amount - len(ret))
 | 
			
		||||
            except AttributeError as err:
 | 
			
		||||
                raise APIConnectionError("Socket was closed") from err
 | 
			
		||||
            except socket.timeout:
 | 
			
		||||
                continue
 | 
			
		||||
            except OSError as err:
 | 
			
		||||
                raise APIConnectionError(f"Error while receiving data: {err}") from err
 | 
			
		||||
            ret += val
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def _recv_varint(self):
 | 
			
		||||
        raw = bytes()
 | 
			
		||||
        while not raw or raw[-1] & 0x80:
 | 
			
		||||
            raw += self._recv(1)
 | 
			
		||||
        return _bytes_to_varuint(raw)
 | 
			
		||||
 | 
			
		||||
    def _run_once(self):
 | 
			
		||||
        if not self._socket_open_event.wait(0.1):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Preamble
 | 
			
		||||
        if self._recv(1)[0] != 0x00:
 | 
			
		||||
            raise APIConnectionError("Invalid preamble")
 | 
			
		||||
 | 
			
		||||
        length = self._recv_varint()
 | 
			
		||||
        msg_type = self._recv_varint()
 | 
			
		||||
 | 
			
		||||
        raw_msg = self._recv(length)
 | 
			
		||||
        if msg_type not in MESSAGE_TYPE_TO_PROTO:
 | 
			
		||||
            _LOGGER.debug("Skipping message type %s", msg_type)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
 | 
			
		||||
        msg.ParseFromString(raw_msg)
 | 
			
		||||
        _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg)))
 | 
			
		||||
        for msg_handler in self._message_handlers[:]:
 | 
			
		||||
            msg_handler(msg)
 | 
			
		||||
        self._handle_internal_messages(msg)
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        self._running_event.set()
 | 
			
		||||
        while not self.stopped:
 | 
			
		||||
            try:
 | 
			
		||||
                self._run_once()
 | 
			
		||||
            except APIConnectionError as err:
 | 
			
		||||
                if self.stopped:
 | 
			
		||||
                    break
 | 
			
		||||
                if self._connected:
 | 
			
		||||
                    _LOGGER.error("Error while reading incoming messages: %s", err)
 | 
			
		||||
                    self._fatal_error(err)
 | 
			
		||||
        self._running_event.clear()
 | 
			
		||||
 | 
			
		||||
    def _handle_internal_messages(self, msg):
 | 
			
		||||
        if isinstance(msg, pb.DisconnectRequest):
 | 
			
		||||
            self._send_message(pb.DisconnectResponse())
 | 
			
		||||
            if self._socket is not None:
 | 
			
		||||
                self._socket.close()
 | 
			
		||||
                self._socket = None
 | 
			
		||||
            self._connected = False
 | 
			
		||||
            if self.on_disconnect is not None:
 | 
			
		||||
                self.on_disconnect(None)
 | 
			
		||||
        elif isinstance(msg, pb.PingRequest):
 | 
			
		||||
            self._send_message(pb.PingResponse())
 | 
			
		||||
        elif isinstance(msg, pb.GetTimeRequest):
 | 
			
		||||
            resp = pb.GetTimeResponse()
 | 
			
		||||
            resp.epoch_seconds = int(time.time())
 | 
			
		||||
            self._send_message(resp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_logs(config, address):
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    port = conf[CONF_PORT]
 | 
			
		||||
    password = conf[CONF_PASSWORD]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
 | 
			
		||||
    cli = APIClient(address, port, password)
 | 
			
		||||
    stopping = False
 | 
			
		||||
    retry_timer = []
 | 
			
		||||
 | 
			
		||||
    has_connects = []
 | 
			
		||||
 | 
			
		||||
    def try_connect(err, tries=0):
 | 
			
		||||
        if stopping:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if err:
 | 
			
		||||
            _LOGGER.warning("Disconnected from API: %s", err)
 | 
			
		||||
 | 
			
		||||
        while retry_timer:
 | 
			
		||||
            retry_timer.pop(0).cancel()
 | 
			
		||||
 | 
			
		||||
        error = None
 | 
			
		||||
        try:
 | 
			
		||||
            cli.connect()
 | 
			
		||||
            cli.login()
 | 
			
		||||
        except APIConnectionError as err2:  # noqa
 | 
			
		||||
            error = err2
 | 
			
		||||
 | 
			
		||||
        if error is None:
 | 
			
		||||
            _LOGGER.info("Successfully connected to %s", address)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        wait_time = int(min(1.5 ** min(tries, 100), 30))
 | 
			
		||||
        if not has_connects:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Initial connection failed. The ESP might not be connected "
 | 
			
		||||
                "to WiFi yet (%s). Re-Trying in %s seconds",
 | 
			
		||||
                error,
 | 
			
		||||
                wait_time,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Couldn't connect to API (%s). Trying to reconnect in %s seconds",
 | 
			
		||||
                error,
 | 
			
		||||
                wait_time,
 | 
			
		||||
            )
 | 
			
		||||
        timer = threading.Timer(
 | 
			
		||||
            wait_time, functools.partial(try_connect, None, tries + 1)
 | 
			
		||||
        )
 | 
			
		||||
        timer.start()
 | 
			
		||||
        retry_timer.append(timer)
 | 
			
		||||
 | 
			
		||||
    def on_log(msg):
 | 
			
		||||
        time_ = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
        text = msg.message
 | 
			
		||||
        if msg.send_failed:
 | 
			
		||||
            text = color(
 | 
			
		||||
                Fore.WHITE,
 | 
			
		||||
                "(Message skipped because it was too big to fit in "
 | 
			
		||||
                "TCP buffer - This is only cosmetic)",
 | 
			
		||||
            )
 | 
			
		||||
        safe_print(time_ + text)
 | 
			
		||||
 | 
			
		||||
    def on_login():
 | 
			
		||||
        try:
 | 
			
		||||
            cli.subscribe_logs(on_log, dump_config=not has_connects)
 | 
			
		||||
            has_connects.append(True)
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            cli.disconnect()
 | 
			
		||||
 | 
			
		||||
    cli.on_disconnect = try_connect
 | 
			
		||||
    cli.on_login = on_login
 | 
			
		||||
    cli.start()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        try_connect(None)
 | 
			
		||||
        while True:
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        stopping = True
 | 
			
		||||
        cli.stop(True)
 | 
			
		||||
        while retry_timer:
 | 
			
		||||
            retry_timer.pop(0).cancel()
 | 
			
		||||
    return 0
 | 
			
		||||
@@ -10,7 +10,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_TYPE_ID,
 | 
			
		||||
    CONF_TIME,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import coroutine
 | 
			
		||||
from esphome.jsonschema import jschema_extractor
 | 
			
		||||
from esphome.util import Registry
 | 
			
		||||
 | 
			
		||||
@@ -142,27 +141,27 @@ NotCondition = cg.esphome_ns.class_("NotCondition", Condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("and", AndCondition, validate_condition_list)
 | 
			
		||||
def and_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    conditions = yield build_condition_list(config, template_arg, args)
 | 
			
		||||
    yield cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
async def and_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    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)
 | 
			
		||||
def or_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    conditions = yield build_condition_list(config, template_arg, args)
 | 
			
		||||
    yield cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
async def or_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    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)
 | 
			
		||||
def not_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    condition = yield build_condition(config, template_arg, args)
 | 
			
		||||
    yield cg.new_Pvariable(condition_id, template_arg, condition)
 | 
			
		||||
async def not_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    condition = await build_condition(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("lambda", LambdaCondition, cv.lambda_)
 | 
			
		||||
def lambda_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    lambda_ = yield cg.process_lambda(config, args, return_type=bool)
 | 
			
		||||
    yield cg.new_Pvariable(condition_id, template_arg, lambda_)
 | 
			
		||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
 | 
			
		||||
async def lambda_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=bool)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
@@ -177,26 +176,26 @@ def lambda_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
)
 | 
			
		||||
def for_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    condition = yield build_condition(
 | 
			
		||||
async def for_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    condition = await build_condition(
 | 
			
		||||
        config[CONF_CONDITION], cg.TemplateArguments(), []
 | 
			
		||||
    )
 | 
			
		||||
    var = cg.new_Pvariable(condition_id, template_arg, condition)
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    templ = yield cg.templatable(config[CONF_TIME], args, cg.uint32)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    templ = await cg.templatable(config[CONF_TIME], args, cg.uint32)
 | 
			
		||||
    cg.add(var.set_time(templ))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
    "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
 | 
			
		||||
)
 | 
			
		||||
def delay_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def delay_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    yield cg.register_component(var, {})
 | 
			
		||||
    template_ = yield cg.templatable(config, args, cg.uint32)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    template_ = await cg.templatable(config, args, cg.uint32)
 | 
			
		||||
    cg.add(var.set_delay(template_))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
@@ -211,16 +210,16 @@ def delay_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
def if_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
async def if_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)
 | 
			
		||||
    if CONF_THEN in config:
 | 
			
		||||
        actions = yield build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
        actions = await build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
        cg.add(var.add_then(actions))
 | 
			
		||||
    if CONF_ELSE in config:
 | 
			
		||||
        actions = yield build_action_list(config[CONF_ELSE], template_arg, args)
 | 
			
		||||
        actions = await build_action_list(config[CONF_ELSE], template_arg, args)
 | 
			
		||||
        cg.add(var.add_else(actions))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
@@ -233,12 +232,12 @@ def if_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
def while_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = yield build_condition(config[CONF_CONDITION], 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)
 | 
			
		||||
    actions = yield build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
    actions = await build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
    cg.add(var.add_then(actions))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_wait_until(value):
 | 
			
		||||
@@ -253,17 +252,17 @@ def validate_wait_until(value):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("wait_until", WaitUntilAction, validate_wait_until)
 | 
			
		||||
def wait_until_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
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)
 | 
			
		||||
    yield cg.register_component(var, {})
 | 
			
		||||
    yield var
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("lambda", LambdaAction, cv.lambda_)
 | 
			
		||||
def lambda_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    lambda_ = yield cg.process_lambda(config, args, return_type=cg.void)
 | 
			
		||||
    yield cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
async def lambda_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
@@ -275,54 +274,51 @@ def lambda_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
def component_update_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    comp = yield cg.get_variable(config[CONF_ID])
 | 
			
		||||
    yield cg.new_Pvariable(action_id, template_arg, comp)
 | 
			
		||||
async def component_update_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    comp = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, comp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine
 | 
			
		||||
def build_action(full_config, template_arg, args):
 | 
			
		||||
async def build_action(full_config, template_arg, args):
 | 
			
		||||
    registry_entry, config = cg.extract_registry_entry_config(
 | 
			
		||||
        ACTION_REGISTRY, full_config
 | 
			
		||||
    )
 | 
			
		||||
    action_id = full_config[CONF_TYPE_ID]
 | 
			
		||||
    builder = registry_entry.coroutine_fun
 | 
			
		||||
    yield builder(config, action_id, template_arg, args)
 | 
			
		||||
    ret = await builder(config, action_id, template_arg, args)
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine
 | 
			
		||||
def build_action_list(config, templ, arg_type):
 | 
			
		||||
async def build_action_list(config, templ, arg_type):
 | 
			
		||||
    actions = []
 | 
			
		||||
    for conf in config:
 | 
			
		||||
        action = yield build_action(conf, templ, arg_type)
 | 
			
		||||
        action = await build_action(conf, templ, arg_type)
 | 
			
		||||
        actions.append(action)
 | 
			
		||||
    yield actions
 | 
			
		||||
    return actions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine
 | 
			
		||||
def build_condition(full_config, template_arg, args):
 | 
			
		||||
async def build_condition(full_config, template_arg, args):
 | 
			
		||||
    registry_entry, config = cg.extract_registry_entry_config(
 | 
			
		||||
        CONDITION_REGISTRY, full_config
 | 
			
		||||
    )
 | 
			
		||||
    action_id = full_config[CONF_TYPE_ID]
 | 
			
		||||
    builder = registry_entry.coroutine_fun
 | 
			
		||||
    yield builder(config, action_id, template_arg, args)
 | 
			
		||||
    ret = await builder(config, action_id, template_arg, args)
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine
 | 
			
		||||
def build_condition_list(config, templ, args):
 | 
			
		||||
async def build_condition_list(config, templ, args):
 | 
			
		||||
    conditions = []
 | 
			
		||||
    for conf in config:
 | 
			
		||||
        condition = yield build_condition(conf, templ, args)
 | 
			
		||||
        condition = await build_condition(conf, templ, args)
 | 
			
		||||
        conditions.append(condition)
 | 
			
		||||
    yield conditions
 | 
			
		||||
    return conditions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine
 | 
			
		||||
def build_automation(trigger, args, config):
 | 
			
		||||
async def build_automation(trigger, args, config):
 | 
			
		||||
    arg_types = [arg[0] for arg in args]
 | 
			
		||||
    templ = cg.TemplateArguments(*arg_types)
 | 
			
		||||
    obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
 | 
			
		||||
    actions = yield build_action_list(config[CONF_THEN], templ, args)
 | 
			
		||||
    actions = await build_action_list(config[CONF_THEN], templ, args)
 | 
			
		||||
    cg.add(obj.add_actions(actions))
 | 
			
		||||
    yield obj
 | 
			
		||||
    return obj
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1152
									
								
								esphome/boards.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1152
									
								
								esphome/boards.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -19,6 +19,7 @@ from esphome.cpp_generator import (  # noqa
 | 
			
		||||
    Statement,
 | 
			
		||||
    LineComment,
 | 
			
		||||
    progmem_array,
 | 
			
		||||
    static_const_array,
 | 
			
		||||
    statement,
 | 
			
		||||
    variable,
 | 
			
		||||
    new_variable,
 | 
			
		||||
@@ -59,6 +60,7 @@ from esphome.cpp_types import (  # noqa
 | 
			
		||||
    uint8,
 | 
			
		||||
    uint16,
 | 
			
		||||
    uint32,
 | 
			
		||||
    uint64,
 | 
			
		||||
    int32,
 | 
			
		||||
    const_char_ptr,
 | 
			
		||||
    NAN,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace a4988 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "a4988.stepper";
 | 
			
		||||
static const char *const TAG = "a4988.stepper";
 | 
			
		||||
 | 
			
		||||
void A4988::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up A4988...");
 | 
			
		||||
 
 | 
			
		||||
@@ -18,16 +18,16 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield stepper.register_stepper(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await stepper.register_stepper(var, config)
 | 
			
		||||
 | 
			
		||||
    step_pin = yield cg.gpio_pin_expression(config[CONF_STEP_PIN])
 | 
			
		||||
    step_pin = await cg.gpio_pin_expression(config[CONF_STEP_PIN])
 | 
			
		||||
    cg.add(var.set_step_pin(step_pin))
 | 
			
		||||
    dir_pin = yield cg.gpio_pin_expression(config[CONF_DIR_PIN])
 | 
			
		||||
    dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN])
 | 
			
		||||
    cg.add(var.set_dir_pin(dir_pin))
 | 
			
		||||
 | 
			
		||||
    if CONF_SLEEP_PIN in config:
 | 
			
		||||
        sleep_pin = yield cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
 | 
			
		||||
        sleep_pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
 | 
			
		||||
        cg.add(var.set_sleep_pin(sleep_pin))
 | 
			
		||||
 
 | 
			
		||||
@@ -9,15 +9,15 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ac_dimmer {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "ac_dimmer";
 | 
			
		||||
static const char *const TAG = "ac_dimmer";
 | 
			
		||||
 | 
			
		||||
// Global array to store dimmer objects
 | 
			
		||||
static AcDimmerDataStore *all_dimmers[32];
 | 
			
		||||
static AcDimmerDataStore *all_dimmers[32];  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
/// Time in microseconds the gate should be held high
 | 
			
		||||
/// 10µs should be long enough for most triacs
 | 
			
		||||
/// For reference: BT136 datasheet says 2µs nominal (page 7)
 | 
			
		||||
static uint32_t GATE_ENABLE_TIME = 10;
 | 
			
		||||
static const uint32_t GATE_ENABLE_TIME = 10;
 | 
			
		||||
 | 
			
		||||
/// Function called from timer interrupt
 | 
			
		||||
/// Input is current time in microseconds (micros())
 | 
			
		||||
@@ -125,7 +125,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
 | 
			
		||||
  // Attaching pin interrupts on the same pin will override the previous interupt
 | 
			
		||||
  // Attaching pin interrupts on the same pin will override the previous interrupt
 | 
			
		||||
  // However, the user expects that multiple dimmers sharing the same ZC pin will work.
 | 
			
		||||
  // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers
 | 
			
		||||
  // if any of them are using the same ZC pin, and also trigger the interrupt for *them*.
 | 
			
		||||
 
 | 
			
		||||
@@ -32,18 +32,18 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    # override default min power to 10%
 | 
			
		||||
    if CONF_MIN_POWER not in config:
 | 
			
		||||
        config[CONF_MIN_POWER] = 0.1
 | 
			
		||||
    yield output.register_output(var, config)
 | 
			
		||||
    await output.register_output(var, config)
 | 
			
		||||
 | 
			
		||||
    pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN])
 | 
			
		||||
    pin = await cg.gpio_pin_expression(config[CONF_GATE_PIN])
 | 
			
		||||
    cg.add(var.set_gate_pin(pin))
 | 
			
		||||
    pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
 | 
			
		||||
    pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
 | 
			
		||||
    cg.add(var.set_zero_cross_pin(pin))
 | 
			
		||||
    cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
 | 
			
		||||
    cg.add(var.set_method(config[CONF_METHOD]))
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,7 @@ CONFIG_SCHEMA = cv.Schema({})
 | 
			
		||||
    "Adalight",
 | 
			
		||||
    {cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)},
 | 
			
		||||
)
 | 
			
		||||
def adalight_light_effect_to_code(config, effect_id):
 | 
			
		||||
async def adalight_light_effect_to_code(config, effect_id):
 | 
			
		||||
    effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
 | 
			
		||||
    yield uart.register_uart_device(effect, config)
 | 
			
		||||
 | 
			
		||||
    yield effect
 | 
			
		||||
    await uart.register_uart_device(effect, config)
 | 
			
		||||
    return effect
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace adalight {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "adalight_light_effect";
 | 
			
		||||
static const char *const TAG = "adalight_light_effect";
 | 
			
		||||
 | 
			
		||||
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
 | 
			
		||||
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
 | 
			
		||||
@@ -42,8 +42,9 @@ void AdalightLightEffect::reset_frame_(light::AddressableLight &it) {
 | 
			
		||||
 | 
			
		||||
void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
 | 
			
		||||
  for (int led = it.size(); led-- > 0;) {
 | 
			
		||||
    it[led].set(COLOR_BLACK);
 | 
			
		||||
    it[led].set(Color::BLACK);
 | 
			
		||||
  }
 | 
			
		||||
  it.schedule_show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) {
 | 
			
		||||
@@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
 | 
			
		||||
    it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it.schedule_show();
 | 
			
		||||
  return CONSUMED;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,50 @@ ADC_MODE(ADC_VCC)
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace adc {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "adc";
 | 
			
		||||
static const char *const TAG = "adc";
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; }
 | 
			
		||||
void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
 | 
			
		||||
 | 
			
		||||
inline adc1_channel_t gpio_to_adc1(uint8_t pin) {
 | 
			
		||||
#if CONFIG_IDF_TARGET_ESP32
 | 
			
		||||
  switch (pin) {
 | 
			
		||||
    case 36:
 | 
			
		||||
      return ADC1_CHANNEL_0;
 | 
			
		||||
    case 37:
 | 
			
		||||
      return ADC1_CHANNEL_1;
 | 
			
		||||
    case 38:
 | 
			
		||||
      return ADC1_CHANNEL_2;
 | 
			
		||||
    case 39:
 | 
			
		||||
      return ADC1_CHANNEL_3;
 | 
			
		||||
    case 32:
 | 
			
		||||
      return ADC1_CHANNEL_4;
 | 
			
		||||
    case 33:
 | 
			
		||||
      return ADC1_CHANNEL_5;
 | 
			
		||||
    case 34:
 | 
			
		||||
      return ADC1_CHANNEL_6;
 | 
			
		||||
    case 35:
 | 
			
		||||
      return ADC1_CHANNEL_7;
 | 
			
		||||
    default:
 | 
			
		||||
      return ADC1_CHANNEL_MAX;
 | 
			
		||||
  }
 | 
			
		||||
#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2
 | 
			
		||||
  switch (pin) {
 | 
			
		||||
    case 0:
 | 
			
		||||
      return ADC1_CHANNEL_0;
 | 
			
		||||
    case 1:
 | 
			
		||||
      return ADC1_CHANNEL_1;
 | 
			
		||||
    case 2:
 | 
			
		||||
      return ADC1_CHANNEL_2;
 | 
			
		||||
    case 3:
 | 
			
		||||
      return ADC1_CHANNEL_3;
 | 
			
		||||
    case 4:
 | 
			
		||||
      return ADC1_CHANNEL_4;
 | 
			
		||||
    default:
 | 
			
		||||
      return ADC1_CHANNEL_MAX;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
@@ -21,7 +61,11 @@ void ADCSensor::setup() {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  analogSetPinAttenuation(this->pin_, this->attenuation_);
 | 
			
		||||
  adc1_config_channel_atten(gpio_to_adc1(pin_), attenuation_);
 | 
			
		||||
  adc1_config_width(ADC_WIDTH_BIT_12);
 | 
			
		||||
#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2
 | 
			
		||||
  adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_));
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void ADCSensor::dump_config() {
 | 
			
		||||
@@ -36,18 +80,20 @@ void ADCSensor::dump_config() {
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_);
 | 
			
		||||
  switch (this->attenuation_) {
 | 
			
		||||
    case ADC_0db:
 | 
			
		||||
    case ADC_ATTEN_DB_0:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_2_5db:
 | 
			
		||||
    case ADC_ATTEN_DB_2_5:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_6db:
 | 
			
		||||
    case ADC_ATTEN_DB_6:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_11db:
 | 
			
		||||
    case ADC_ATTEN_DB_11:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
 | 
			
		||||
      break;
 | 
			
		||||
    default:  // This is to satisfy the unused ADC_ATTEN_MAX
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
@@ -60,21 +106,43 @@ void ADCSensor::update() {
 | 
			
		||||
}
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  float value_v = analogRead(this->pin_) / 4095.0f;  // NOLINT
 | 
			
		||||
  int raw = adc1_get_raw(gpio_to_adc1(pin_));
 | 
			
		||||
  float value_v = raw / 4095.0f;
 | 
			
		||||
#if CONFIG_IDF_TARGET_ESP32
 | 
			
		||||
  switch (this->attenuation_) {
 | 
			
		||||
    case ADC_0db:
 | 
			
		||||
    case ADC_ATTEN_DB_0:
 | 
			
		||||
      value_v *= 1.1;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_2_5db:
 | 
			
		||||
    case ADC_ATTEN_DB_2_5:
 | 
			
		||||
      value_v *= 1.5;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_6db:
 | 
			
		||||
    case ADC_ATTEN_DB_6:
 | 
			
		||||
      value_v *= 2.2;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_11db:
 | 
			
		||||
    case ADC_ATTEN_DB_11:
 | 
			
		||||
      value_v *= 3.9;
 | 
			
		||||
      break;
 | 
			
		||||
    default:  // This is to satisfy the unused ADC_ATTEN_MAX
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2
 | 
			
		||||
  switch (this->attenuation_) {
 | 
			
		||||
    case ADC_ATTEN_DB_0:
 | 
			
		||||
      value_v *= 0.84;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_ATTEN_DB_2_5:
 | 
			
		||||
      value_v *= 1.13;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_ATTEN_DB_6:
 | 
			
		||||
      value_v *= 1.56;
 | 
			
		||||
      break;
 | 
			
		||||
    case ADC_ATTEN_DB_11:
 | 
			
		||||
      value_v *= 3.0;
 | 
			
		||||
      break;
 | 
			
		||||
    default:  // This is to satisfy the unused ADC_ATTEN_MAX
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  return value_v;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,10 @@
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/voltage_sampler/voltage_sampler.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#include "driver/adc.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace adc {
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +17,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
 public:
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  /// Set the attenuation for this pin. Only available on the ESP32.
 | 
			
		||||
  void set_attenuation(adc_attenuation_t attenuation);
 | 
			
		||||
  void set_attenuation(adc_atten_t attenuation);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /// Update adc values.
 | 
			
		||||
@@ -34,7 +38,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
  uint8_t pin_;
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  adc_attenuation_t attenuation_{ADC_0db};
 | 
			
		||||
  adc_atten_t attenuation_{ADC_ATTEN_DB_0};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -15,10 +15,10 @@ from esphome.const import (
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
 | 
			
		||||
ATTENUATION_MODES = {
 | 
			
		||||
    "0db": cg.global_ns.ADC_0db,
 | 
			
		||||
    "2.5db": cg.global_ns.ADC_2_5db,
 | 
			
		||||
    "6db": cg.global_ns.ADC_6db,
 | 
			
		||||
    "11db": cg.global_ns.ADC_11db,
 | 
			
		||||
    "0db": cg.global_ns.ADC_ATTEN_DB_0,
 | 
			
		||||
    "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5,
 | 
			
		||||
    "6db": cg.global_ns.ADC_ATTEN_DB_6,
 | 
			
		||||
    "11db": cg.global_ns.ADC_ATTEN_DB_11,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +35,12 @@ ADCSensor = adc_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE)
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=2,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADCSensor),
 | 
			
		||||
@@ -49,10 +54,10 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield sensor.register_sensor(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    if config[CONF_PIN] == "VCC":
 | 
			
		||||
        cg.add_define("USE_ADC_SENSOR_VCC")
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace addressable_light {
 | 
			
		||||
 | 
			
		||||
static const char* TAG = "addressable_light.display";
 | 
			
		||||
static const char *const TAG = "addressable_light.display";
 | 
			
		||||
 | 
			
		||||
int AddressableLightDisplay::get_width_internal() { return this->width_; }
 | 
			
		||||
int AddressableLightDisplay::get_height_internal() { return this->height_; }
 | 
			
		||||
@@ -24,7 +24,7 @@ void AddressableLightDisplay::update() {
 | 
			
		||||
void AddressableLightDisplay::display() {
 | 
			
		||||
  bool dirty = false;
 | 
			
		||||
  uint8_t old_r, old_g, old_b, old_w;
 | 
			
		||||
  Color* c;
 | 
			
		||||
  Color *c;
 | 
			
		||||
 | 
			
		||||
  for (uint32_t offset = 0; offset < this->addressable_light_buffer_.size(); offset++) {
 | 
			
		||||
    c = &(this->addressable_light_buffer_[offset]);
 | 
			
		||||
 
 | 
			
		||||
@@ -38,18 +38,18 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    wrapped_light = yield cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
 | 
			
		||||
    wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
 | 
			
		||||
    cg.add(var.set_width(config[CONF_WIDTH]))
 | 
			
		||||
    cg.add(var.set_height(config[CONF_HEIGHT]))
 | 
			
		||||
    cg.add(var.set_light(wrapped_light))
 | 
			
		||||
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield display.register_display(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await display.register_display(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_PIXEL_MAPPER in config:
 | 
			
		||||
        pixel_mapper_template_ = yield cg.process_lambda(
 | 
			
		||||
        pixel_mapper_template_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_PIXEL_MAPPER],
 | 
			
		||||
            [(int, "x"), (int, "y")],
 | 
			
		||||
            return_type=cg.int_,
 | 
			
		||||
@@ -57,7 +57,7 @@ def to_code(config):
 | 
			
		||||
        cg.add(var.set_pixel_mapper(pixel_mapper_template_))
 | 
			
		||||
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = yield cg.process_lambda(
 | 
			
		||||
        lambda_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ade7953 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "ade7953";
 | 
			
		||||
static const char *const TAG = "ade7953";
 | 
			
		||||
 | 
			
		||||
void ADE7953::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ADE7953:");
 | 
			
		||||
@@ -21,8 +21,8 @@ void ADE7953::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define ADE_PUBLISH_(name, factor) \
 | 
			
		||||
  if (name && this->name##_sensor_) { \
 | 
			
		||||
    float value = *name / factor; \
 | 
			
		||||
  if ((name) && this->name##_sensor_) { \
 | 
			
		||||
    float value = *(name) / (factor); \
 | 
			
		||||
    this->name##_sensor_->publish_state(value); \
 | 
			
		||||
  }
 | 
			
		||||
#define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
@@ -31,19 +31,34 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADE7953),
 | 
			
		||||
            cv.Optional(CONF_IRQ_PIN): pins.input_pin,
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(
 | 
			
		||||
                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(
 | 
			
		||||
                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(
 | 
			
		||||
                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(
 | 
			
		||||
                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -52,10 +67,10 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield i2c.register_i2c_device(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_IRQ_PIN in config:
 | 
			
		||||
        cg.add(var.set_irq_pin(config[CONF_IRQ_PIN]))
 | 
			
		||||
@@ -70,5 +85,5 @@ def to_code(config):
 | 
			
		||||
        if key not in config:
 | 
			
		||||
            continue
 | 
			
		||||
        conf = config[key]
 | 
			
		||||
        sens = yield sensor.new_sensor(conf)
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(getattr(var, f"set_{key}_sensor")(sens))
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,9 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield i2c.register_i2c_device(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ads1115 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "ads1115";
 | 
			
		||||
static const char *const TAG = "ads1115";
 | 
			
		||||
static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
 | 
			
		||||
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
 | 
			
		||||
 | 
			
		||||
@@ -64,11 +64,6 @@ void ADS1115Component::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->prev_config_ = config;
 | 
			
		||||
 | 
			
		||||
  for (auto *sensor : this->sensors_) {
 | 
			
		||||
    this->set_interval(sensor->get_name(), sensor->update_interval(),
 | 
			
		||||
                       [this, sensor] { this->request_measurement(sensor); });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void ADS1115Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
 | 
			
		||||
@@ -107,17 +102,22 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
 | 
			
		||||
    }
 | 
			
		||||
    this->prev_config_ = config;
 | 
			
		||||
 | 
			
		||||
    // about 1.6 ms with 860 samples per second
 | 
			
		||||
    // about 1.2 ms with 860 samples per second
 | 
			
		||||
    delay(2);
 | 
			
		||||
 | 
			
		||||
    uint32_t start = millis();
 | 
			
		||||
    while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) {
 | 
			
		||||
      if (millis() - start > 100) {
 | 
			
		||||
        ESP_LOGW(TAG, "Reading ADS1115 timed out");
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        return NAN;
 | 
			
		||||
    // in continuous mode, conversion will always be running, rely on the delay
 | 
			
		||||
    // to ensure conversion is taking place with the correct settings
 | 
			
		||||
    // can we use the rdy pin to trigger when a conversion is done?
 | 
			
		||||
    if (!this->continuous_mode_) {
 | 
			
		||||
      uint32_t start = millis();
 | 
			
		||||
      while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) {
 | 
			
		||||
        if (millis() - start > 100) {
 | 
			
		||||
          ESP_LOGW(TAG, "Reading ADS1115 timed out");
 | 
			
		||||
          this->status_set_warning();
 | 
			
		||||
          return NAN;
 | 
			
		||||
        }
 | 
			
		||||
        yield();
 | 
			
		||||
      }
 | 
			
		||||
      yield();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    CONF_MULTIPLEXER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
@@ -51,7 +51,12 @@ ADS1115Sensor = ads1115_ns.class_(
 | 
			
		||||
 | 
			
		||||
CONF_ADS1115_ID = "ads1115_id"
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE)
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=3,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADS1115Sensor),
 | 
			
		||||
@@ -64,11 +69,11 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    paren = yield cg.get_variable(config[CONF_ADS1115_ID])
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ADS1115_ID])
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], paren)
 | 
			
		||||
    yield sensor.register_sensor(var, config)
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
 | 
			
		||||
    cg.add(var.set_gain(config[CONF_GAIN]))
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
//
 | 
			
		||||
// According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost
 | 
			
		||||
// immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best
 | 
			
		||||
// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time.
 | 
			
		||||
// results making successive requests; the current implementation makes 3 attempts with a delay of 30ms each time.
 | 
			
		||||
 | 
			
		||||
#include "aht10.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
@@ -18,12 +18,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace aht10 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "aht10";
 | 
			
		||||
static const char *const TAG = "aht10";
 | 
			
		||||
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1};
 | 
			
		||||
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
 | 
			
		||||
static const uint8_t AHT10_DEFAULT_DELAY = 5;    // ms, for calibration and temperature measurement
 | 
			
		||||
static const uint8_t AHT10_HUMIDITY_DELAY = 30;  // ms
 | 
			
		||||
static const uint8_t AHT10_ATTEMPS = 3;          // safety margin, normally 3 attemps are enough: 3*30=90ms
 | 
			
		||||
static const uint8_t AHT10_ATTEMPTS = 3;         // safety margin, normally 3 attempts are enough: 3*30=90ms
 | 
			
		||||
 | 
			
		||||
void AHT10Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AHT10...");
 | 
			
		||||
@@ -58,8 +58,9 @@ void AHT10Component::update() {
 | 
			
		||||
  uint8_t delay = AHT10_DEFAULT_DELAY;
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    delay = AHT10_HUMIDITY_DELAY;
 | 
			
		||||
  for (int i = 0; i < AHT10_ATTEMPS; ++i) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis());
 | 
			
		||||
  for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Attempt %u at %6ld", i, millis());
 | 
			
		||||
    delay_microseconds_accurate(4);
 | 
			
		||||
    if (!this->read_bytes(0, data, 6, delay)) {
 | 
			
		||||
      ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
 | 
			
		||||
    } else if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
@@ -22,10 +22,16 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AHT10Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                UNIT_PERCENT, ICON_EMPTY, 2, DEVICE_CLASS_HUMIDITY
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -34,15 +40,15 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield i2c.register_i2c_device(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import esp32_ble_tracker
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
CODEOWNERS = ["@jeromelaban"]
 | 
			
		||||
 | 
			
		||||
airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
 | 
			
		||||
AirthingsListener = airthings_ble_ns.class_(
 | 
			
		||||
    "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(AirthingsListener),
 | 
			
		||||
    }
 | 
			
		||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield esp32_ble_tracker.register_ble_device(var, config)
 | 
			
		||||
							
								
								
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
#include "airthings_listener.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_ble {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "airthings_ble";
 | 
			
		||||
 | 
			
		||||
bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  for (auto &it : device.get_manufacturer_datas()) {
 | 
			
		||||
    if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
 | 
			
		||||
      if (it.data.size() < 4)
 | 
			
		||||
        continue;
 | 
			
		||||
 | 
			
		||||
      uint32_t sn = it.data[0];
 | 
			
		||||
      sn |= ((uint32_t) it.data[1] << 8);
 | 
			
		||||
      sn |= ((uint32_t) it.data[2] << 16);
 | 
			
		||||
      sn |= ((uint32_t) it.data[3] << 24);
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_ble
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include <BLEDevice.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_ble {
 | 
			
		||||
 | 
			
		||||
class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
 | 
			
		||||
 public:
 | 
			
		||||
  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_ble
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@jeromelaban"]
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
#include "airthings_wave_plus.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                            esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      if (param->open.status == ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGI(TAG, "Connected successfully!");
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnected!");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      this->handle = 0;
 | 
			
		||||
      auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(),
 | 
			
		||||
                 sensors_data_characteristic_uuid.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->handle = chr->handle;
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
 | 
			
		||||
      request_read_values_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.conn_id != this->parent()->conn_id)
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->read.handle == this->handle) {
 | 
			
		||||
        read_sensors_(param->read.value, param->read.value_len);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
  auto value = (WavePlusReadings *) raw_value;
 | 
			
		||||
 | 
			
		||||
  if (sizeof(WavePlusReadings) <= value_len) {
 | 
			
		||||
    ESP_LOGD(TAG, "version = %d", value->version);
 | 
			
		||||
 | 
			
		||||
    if (value->version == 1) {
 | 
			
		||||
      ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
 | 
			
		||||
 | 
			
		||||
      this->humidity_sensor_->publish_state(value->humidity / 2.0f);
 | 
			
		||||
      if (is_valid_radon_value_(value->radon)) {
 | 
			
		||||
        this->radon_sensor_->publish_state(value->radon);
 | 
			
		||||
      }
 | 
			
		||||
      if (is_valid_radon_value_(value->radon_lt)) {
 | 
			
		||||
        this->radon_long_term_sensor_->publish_state(value->radon_lt);
 | 
			
		||||
      }
 | 
			
		||||
      this->temperature_sensor_->publish_state(value->temperature / 100.0f);
 | 
			
		||||
      this->pressure_sensor_->publish_state(value->pressure / 50.0f);
 | 
			
		||||
      if (is_valid_co2_value_(value->co2)) {
 | 
			
		||||
        this->co2_sensor_->publish_state(value->co2);
 | 
			
		||||
      }
 | 
			
		||||
      if (is_valid_voc_value_(value->voc)) {
 | 
			
		||||
        this->tvoc_sensor_->publish_state(value->voc);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // This instance must not stay connected
 | 
			
		||||
      // so other clients can connect to it (e.g. the
 | 
			
		||||
      // mobile app).
 | 
			
		||||
      parent()->set_enabled(false);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::loop() {}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
    if (!parent()->enabled) {
 | 
			
		||||
      ESP_LOGW(TAG, "Reconnecting to device");
 | 
			
		||||
      parent()->set_enabled(true);
 | 
			
		||||
      parent()->connect();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Connection in progress");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::request_read_values_() {
 | 
			
		||||
  auto status =
 | 
			
		||||
      esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Radon", this->radon_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
 | 
			
		||||
  auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
 | 
			
		||||
  auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();
 | 
			
		||||
 | 
			
		||||
  service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
 | 
			
		||||
  sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::setup() {}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
							
								
								
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <BLEDevice.h>
 | 
			
		||||
 | 
			
		||||
using namespace esphome::ble_client;
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "airthings_wave_plus";
 | 
			
		||||
 | 
			
		||||
class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  AirthingsWavePlus();
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
 | 
			
		||||
  void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
 | 
			
		||||
  void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
 | 
			
		||||
  void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
 | 
			
		||||
  void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
 | 
			
		||||
  void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_valid_radon_value_(short radon);
 | 
			
		||||
  bool is_valid_voc_value_(short voc);
 | 
			
		||||
  bool is_valid_co2_value_(short co2);
 | 
			
		||||
 | 
			
		||||
  void read_sensors_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  void request_read_values_();
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *radon_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *radon_long_term_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *pressure_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *co2_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *tvoc_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid;
 | 
			
		||||
  espbt::ESPBTUUID sensors_data_characteristic_uuid;
 | 
			
		||||
 | 
			
		||||
  struct WavePlusReadings {
 | 
			
		||||
    uint8_t version;
 | 
			
		||||
    uint8_t humidity;
 | 
			
		||||
    uint8_t ambientLight;
 | 
			
		||||
    uint8_t unused01;
 | 
			
		||||
    uint16_t radon;
 | 
			
		||||
    uint16_t radon_lt;
 | 
			
		||||
    uint16_t temperature;
 | 
			
		||||
    uint16_t pressure;
 | 
			
		||||
    uint16_t co2;
 | 
			
		||||
    uint16_t voc;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
							
								
								
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    ICON_RADIOACTIVE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RADON,
 | 
			
		||||
    CONF_RADON_LONG_TERM,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_CO2,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
 | 
			
		||||
AirthingsWavePlus = airthings_wave_plus_ns.class_(
 | 
			
		||||
    "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RADON): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_RADIOACTIVE,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_RADIOACTIVE,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CO2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TVOC): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_BILLION,
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("5mins"))
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        cg.add(var.set_humidity(sens))
 | 
			
		||||
    if CONF_RADON in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON])
 | 
			
		||||
        cg.add(var.set_radon(sens))
 | 
			
		||||
    if CONF_RADON_LONG_TERM in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
 | 
			
		||||
        cg.add(var.set_radon_long_term(sens))
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_temperature(sens))
 | 
			
		||||
    if CONF_PRESSURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_PRESSURE])
 | 
			
		||||
        cg.add(var.set_pressure(sens))
 | 
			
		||||
    if CONF_CO2 in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_CO2])
 | 
			
		||||
        cg.add(var.set_co2(sens))
 | 
			
		||||
    if CONF_TVOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
        cg.add(var.set_tvoc(sens))
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am2320 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "am2320";
 | 
			
		||||
static const char *const TAG = "am2320";
 | 
			
		||||
 | 
			
		||||
// ---=== Calc CRC16 ===---
 | 
			
		||||
uint16_t crc_16(uint8_t *ptr, uint8_t length) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -24,10 +24,16 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AM2320Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -36,15 +42,15 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield i2c.register_i2c_device(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										115
									
								
								esphome/components/am43/am43.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								esphome/components/am43/am43.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
#include "am43.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "am43";
 | 
			
		||||
 | 
			
		||||
void Am43::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "AM43");
 | 
			
		||||
  LOG_SENSOR(" ", "Battery", this->battery_);
 | 
			
		||||
  LOG_SENSOR(" ", "Illuminance", this->illuminance_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43::setup() {
 | 
			
		||||
  this->encoder_ = new Am43Encoder();
 | 
			
		||||
  this->decoder_ = new Am43Decoder();
 | 
			
		||||
  this->logged_in_ = false;
 | 
			
		||||
  this->last_battery_update_ = 0;
 | 
			
		||||
  this->current_sensor_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      this->logged_in_ = false;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->logged_in_ = false;
 | 
			
		||||
      this->node_state = espbt::ClientState::Idle;
 | 
			
		||||
      if (this->battery_ != nullptr)
 | 
			
		||||
        this->battery_->publish_state(NAN);
 | 
			
		||||
      if (this->illuminance_ != nullptr)
 | 
			
		||||
        this->illuminance_->publish_state(NAN);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
 | 
			
		||||
          ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
 | 
			
		||||
                   this->parent_->address_str().c_str());
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
 | 
			
		||||
                   this->parent_->address_str().c_str());
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->char_handle_ = chr->handle;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      this->update();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.handle != this->char_handle_)
 | 
			
		||||
        break;
 | 
			
		||||
      this->decoder_->decode(param->notify.value, param->notify.value_len);
 | 
			
		||||
 | 
			
		||||
      if (this->battery_ != nullptr && this->decoder_->has_battery_level() &&
 | 
			
		||||
          millis() - this->last_battery_update_ > 10000) {
 | 
			
		||||
        this->battery_->publish_state(this->decoder_->battery_level_);
 | 
			
		||||
        this->last_battery_update_ = millis();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) {
 | 
			
		||||
        this->illuminance_->publish_state(this->decoder_->light_level_);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->current_sensor_ > 0) {
 | 
			
		||||
        if (this->illuminance_ != nullptr) {
 | 
			
		||||
          auto packet = this->encoder_->get_light_level_request();
 | 
			
		||||
          auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
 | 
			
		||||
                                                 packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP,
 | 
			
		||||
                                                 ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
          if (status)
 | 
			
		||||
            ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
 | 
			
		||||
                     status);
 | 
			
		||||
        }
 | 
			
		||||
        this->current_sensor_ = 0;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_sensor_ == 0) {
 | 
			
		||||
    if (this->battery_ != nullptr) {
 | 
			
		||||
      auto packet = this->encoder_->get_battery_level_request();
 | 
			
		||||
      auto status =
 | 
			
		||||
          esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
 | 
			
		||||
                                   packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
      if (status)
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    }
 | 
			
		||||
    this->current_sensor_++;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										45
									
								
								esphome/components/am43/am43.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/am43/am43.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/am43/am43_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  void set_battery(sensor::Sensor *battery) { battery_ = battery; }
 | 
			
		||||
  void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  Am43Encoder *encoder_;
 | 
			
		||||
  Am43Decoder *decoder_;
 | 
			
		||||
  bool logged_in_;
 | 
			
		||||
  sensor::Sensor *battery_{nullptr};
 | 
			
		||||
  sensor::Sensor *illuminance_{nullptr};
 | 
			
		||||
  uint8_t current_sensor_;
 | 
			
		||||
  // The AM43 often gets into a state where it spams loads of battery update
 | 
			
		||||
  // notifications. Here we will limit to no more than every 10s.
 | 
			
		||||
  uint8_t last_battery_update_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/components/am43/am43_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/am43/am43_base.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
#include "am43_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
 | 
			
		||||
 | 
			
		||||
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
 | 
			
		||||
  char buf[64];
 | 
			
		||||
  memset(buf, 0, 64);
 | 
			
		||||
  for (int i = 0; i < len; i++)
 | 
			
		||||
    sprintf(&buf[i * 2], "%02x", data[i]);
 | 
			
		||||
  std::string ret = buf;
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_battery_level_request() {
 | 
			
		||||
  uint8_t data = 0x1;
 | 
			
		||||
  return this->encode_(0xA2, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_light_level_request() {
 | 
			
		||||
  uint8_t data = 0x1;
 | 
			
		||||
  return this->encode_(0xAA, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_position_request() {
 | 
			
		||||
  uint8_t data = 0x1;
 | 
			
		||||
  return this->encode_(CMD_GET_POSITION, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) {
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
  data[0] = (pin & 0xFF00) >> 8;
 | 
			
		||||
  data[1] = pin & 0xFF;
 | 
			
		||||
  return this->encode_(CMD_SEND_PIN, data, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_open_request() {
 | 
			
		||||
  uint8_t data = 0xDD;
 | 
			
		||||
  return this->encode_(CMD_SET_STATE, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_close_request() {
 | 
			
		||||
  uint8_t data = 0xEE;
 | 
			
		||||
  return this->encode_(CMD_SET_STATE, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_stop_request() {
 | 
			
		||||
  uint8_t data = 0xCC;
 | 
			
		||||
  return this->encode_(CMD_SET_STATE, &data, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) {
 | 
			
		||||
  return this->encode_(CMD_SET_POSITION, &position, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Encoder::checksum_() {
 | 
			
		||||
  uint8_t checksum = 0;
 | 
			
		||||
  int i = 0;
 | 
			
		||||
  for (i = 0; i < this->packet_.length; i++)
 | 
			
		||||
    checksum = checksum ^ this->packet_.data[i];
 | 
			
		||||
  this->packet_.data[i] = checksum ^ 0xff;
 | 
			
		||||
  this->packet_.length++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) {
 | 
			
		||||
  memcpy(this->packet_.data, START_PACKET, 5);
 | 
			
		||||
  this->packet_.data[5] = command;
 | 
			
		||||
  this->packet_.data[6] = length;
 | 
			
		||||
  memcpy(&this->packet_.data[7], data, length);
 | 
			
		||||
  this->packet_.length = length + 7;
 | 
			
		||||
  this->checksum_();
 | 
			
		||||
  ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str());
 | 
			
		||||
  return &this->packet_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define VERIFY_MIN_LENGTH(x) \
 | 
			
		||||
  if (length < (x)) \
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
 | 
			
		||||
  this->has_battery_level_ = false;
 | 
			
		||||
  this->has_light_level_ = false;
 | 
			
		||||
  this->has_set_position_response_ = false;
 | 
			
		||||
  this->has_set_state_response_ = false;
 | 
			
		||||
  this->has_position_ = false;
 | 
			
		||||
  this->has_pin_response_ = false;
 | 
			
		||||
  ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str());
 | 
			
		||||
 | 
			
		||||
  if (length < 2 || data[0] != 0x9a)
 | 
			
		||||
    return;
 | 
			
		||||
  switch (data[1]) {
 | 
			
		||||
    case CMD_GET_BATTERY_LEVEL: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(8);
 | 
			
		||||
      this->battery_level_ = data[7];
 | 
			
		||||
      this->has_battery_level_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_GET_LIGHT_LEVEL: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(5);
 | 
			
		||||
      this->light_level_ = 100 * ((float) data[4] / 9);
 | 
			
		||||
      this->has_light_level_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_GET_POSITION: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(6);
 | 
			
		||||
      this->position_ = data[5];
 | 
			
		||||
      this->has_position_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_NOTIFY_POSITION: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(5);
 | 
			
		||||
      this->position_ = data[4];
 | 
			
		||||
      this->has_position_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_SEND_PIN: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(4);
 | 
			
		||||
      this->pin_ok_ = data[3] == RESPONSE_ACK;
 | 
			
		||||
      this->has_pin_response_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_SET_POSITION: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(4);
 | 
			
		||||
      this->set_position_ok_ = data[3] == RESPONSE_ACK;
 | 
			
		||||
      this->has_set_position_response_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_SET_STATE: {
 | 
			
		||||
      VERIFY_MIN_LENGTH(4);
 | 
			
		||||
      this->set_state_ok_ = data[3] == RESPONSE_ACK;
 | 
			
		||||
      this->has_set_state_response_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										78
									
								
								esphome/components/am43/am43_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/am43/am43_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
static const uint16_t AM43_SERVICE_UUID = 0xFE50;
 | 
			
		||||
static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51;
 | 
			
		||||
//
 | 
			
		||||
// Tuya identifiers, only to detect and warn users as they are incompatible.
 | 
			
		||||
static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910;
 | 
			
		||||
static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11;
 | 
			
		||||
 | 
			
		||||
struct Am43Packet {
 | 
			
		||||
  uint8_t length;
 | 
			
		||||
  uint8_t data[24];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2;
 | 
			
		||||
static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA;
 | 
			
		||||
static const uint8_t CMD_GET_POSITION = 0xA7;
 | 
			
		||||
static const uint8_t CMD_SEND_PIN = 0x17;
 | 
			
		||||
static const uint8_t CMD_SET_STATE = 0x0A;
 | 
			
		||||
static const uint8_t CMD_SET_POSITION = 0x0D;
 | 
			
		||||
static const uint8_t CMD_NOTIFY_POSITION = 0xA1;
 | 
			
		||||
 | 
			
		||||
static const uint8_t RESPONSE_ACK = 0x5A;
 | 
			
		||||
static const uint8_t RESPONSE_NACK = 0xA5;
 | 
			
		||||
 | 
			
		||||
class Am43Encoder {
 | 
			
		||||
 public:
 | 
			
		||||
  Am43Packet *get_battery_level_request();
 | 
			
		||||
  Am43Packet *get_light_level_request();
 | 
			
		||||
  Am43Packet *get_position_request();
 | 
			
		||||
  Am43Packet *get_send_pin_request(uint16_t pin);
 | 
			
		||||
  Am43Packet *get_open_request();
 | 
			
		||||
  Am43Packet *get_close_request();
 | 
			
		||||
  Am43Packet *get_stop_request();
 | 
			
		||||
  Am43Packet *get_set_position_request(uint8_t position);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void checksum_();
 | 
			
		||||
  Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length);
 | 
			
		||||
  Am43Packet packet_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Am43Decoder {
 | 
			
		||||
 public:
 | 
			
		||||
  void decode(const uint8_t *data, uint16_t length);
 | 
			
		||||
  bool has_battery_level() { return this->has_battery_level_; }
 | 
			
		||||
  bool has_light_level() { return this->has_light_level_; }
 | 
			
		||||
  bool has_set_position_response() { return this->has_set_position_response_; }
 | 
			
		||||
  bool has_set_state_response() { return this->has_set_state_response_; }
 | 
			
		||||
  bool has_position() { return this->has_position_; }
 | 
			
		||||
  bool has_pin_response() { return this->has_pin_response_; }
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    uint8_t position_;
 | 
			
		||||
    uint8_t battery_level_;
 | 
			
		||||
    float light_level_;
 | 
			
		||||
    uint8_t set_position_ok_;
 | 
			
		||||
    uint8_t set_state_ok_;
 | 
			
		||||
    uint8_t pin_ok_;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool has_battery_level_;
 | 
			
		||||
  bool has_light_level_;
 | 
			
		||||
  bool has_set_position_response_;
 | 
			
		||||
  bool has_set_state_response_;
 | 
			
		||||
  bool has_position_;
 | 
			
		||||
  bool has_pin_response_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										36
									
								
								esphome/components/am43/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/am43/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import cover, ble_client
 | 
			
		||||
from esphome.const import CONF_ID, CONF_PIN
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
AUTO_LOAD = ["am43"]
 | 
			
		||||
 | 
			
		||||
CONF_INVERT_POSITION = "invert_position"
 | 
			
		||||
 | 
			
		||||
am43_ns = cg.esphome_ns.namespace("am43")
 | 
			
		||||
Am43Component = am43_ns.class_(
 | 
			
		||||
    "Am43Component", cover.Cover, ble_client.BLEClientNode, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cover.COVER_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Am43Component),
 | 
			
		||||
            cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
 | 
			
		||||
            cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_pin(config[CONF_PIN]))
 | 
			
		||||
    cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield cover.register_cover(var, config)
 | 
			
		||||
    yield ble_client.register_ble_node(var, config)
 | 
			
		||||
							
								
								
									
										149
									
								
								esphome/components/am43/cover/am43_cover.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								esphome/components/am43/cover/am43_cover.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
#include "am43_cover.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "am43_cover";
 | 
			
		||||
 | 
			
		||||
using namespace esphome::cover;
 | 
			
		||||
 | 
			
		||||
void Am43Component::dump_config() {
 | 
			
		||||
  LOG_COVER("", "AM43 Cover", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Device Pin: %d", this->pin_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Invert Position: %d", (int) this->invert_position_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::setup() {
 | 
			
		||||
  this->position = COVER_OPEN;
 | 
			
		||||
  this->encoder_ = new Am43Encoder();
 | 
			
		||||
  this->decoder_ = new Am43Decoder();
 | 
			
		||||
  this->logged_in_ = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::loop() {
 | 
			
		||||
  if (this->node_state == espbt::ClientState::Established && !this->logged_in_) {
 | 
			
		||||
    auto packet = this->encoder_->get_send_pin_request(this->pin_);
 | 
			
		||||
    auto status =
 | 
			
		||||
        esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
 | 
			
		||||
                                 packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str());
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status);
 | 
			
		||||
    else
 | 
			
		||||
      this->logged_in_ = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CoverTraits Am43Component::get_traits() {
 | 
			
		||||
  auto traits = CoverTraits();
 | 
			
		||||
  traits.set_supports_position(true);
 | 
			
		||||
  traits.set_supports_tilt(false);
 | 
			
		||||
  traits.set_is_assumed_state(false);
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::control(const CoverCall &call) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_stop()) {
 | 
			
		||||
    auto packet = this->encoder_->get_stop_request();
 | 
			
		||||
    auto status =
 | 
			
		||||
        esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
 | 
			
		||||
                                 packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_position().has_value()) {
 | 
			
		||||
    auto pos = *call.get_position();
 | 
			
		||||
 | 
			
		||||
    if (this->invert_position_)
 | 
			
		||||
      pos = 1 - pos;
 | 
			
		||||
    auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100));
 | 
			
		||||
    auto status =
 | 
			
		||||
        esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
 | 
			
		||||
                                 packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                        esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->logged_in_ = false;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
 | 
			
		||||
          ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str());
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->get_name().c_str());
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->char_handle_ = chr->handle;
 | 
			
		||||
 | 
			
		||||
      auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle);
 | 
			
		||||
      if (status) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.handle != this->char_handle_)
 | 
			
		||||
        break;
 | 
			
		||||
      this->decoder_->decode(param->notify.value, param->notify.value_len);
 | 
			
		||||
 | 
			
		||||
      if (this->decoder_->has_position()) {
 | 
			
		||||
        this->position = ((float) this->decoder_->position_ / 100.0);
 | 
			
		||||
        if (!this->invert_position_)
 | 
			
		||||
          this->position = 1 - this->position;
 | 
			
		||||
        if (this->position > 0.97)
 | 
			
		||||
          this->position = 1.0;
 | 
			
		||||
        if (this->position < 0.02)
 | 
			
		||||
          this->position = 0.0;
 | 
			
		||||
        this->publish_state();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->decoder_->has_pin_response()) {
 | 
			
		||||
        if (this->decoder_->pin_ok_) {
 | 
			
		||||
          ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str());
 | 
			
		||||
          auto packet = this->encoder_->get_position_request();
 | 
			
		||||
          auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
 | 
			
		||||
                                                 packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP,
 | 
			
		||||
                                                 ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
          if (status)
 | 
			
		||||
            ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status);
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGW(TAG, "[%s] AM43 pin rejected!", this->get_name().c_str());
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_)
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", this->get_name().c_str());
 | 
			
		||||
 | 
			
		||||
      if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_)
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", this->get_name().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										45
									
								
								esphome/components/am43/cover/am43_cover.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/am43/cover/am43_cover.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/cover/cover.h"
 | 
			
		||||
#include "esphome/components/am43/am43_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  cover::CoverTraits get_traits() override;
 | 
			
		||||
  void set_pin(uint16_t pin) { this->pin_ = pin; }
 | 
			
		||||
  void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const cover::CoverCall &call) override;
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  uint16_t pin_;
 | 
			
		||||
  bool invert_position_;
 | 
			
		||||
  Am43Encoder *encoder_;
 | 
			
		||||
  Am43Decoder *decoder_;
 | 
			
		||||
  bool logged_in_;
 | 
			
		||||
 | 
			
		||||
  float position_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace am43
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										46
									
								
								esphome/components/am43/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								esphome/components/am43/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_BATTERY_LEVEL,
 | 
			
		||||
    ICON_BATTERY,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    ICON_BRIGHTNESS_5,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
 | 
			
		||||
am43_ns = cg.esphome_ns.namespace("am43")
 | 
			
		||||
Am43 = am43_ns.class_("Am43", ble_client.BLEClientNode, cg.PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Am43),
 | 
			
		||||
            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
 | 
			
		||||
                UNIT_PERCENT, ICON_BATTERY, 0
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_PERCENT, ICON_BRIGHTNESS_5, 0
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.polling_component_schema("120s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield ble_client.register_ble_node(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_BATTERY_LEVEL in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
 | 
			
		||||
        cg.add(var.set_battery(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_ILLUMINANCE in config:
 | 
			
		||||
        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
 | 
			
		||||
        cg.add(var.set_illuminance(sens))
 | 
			
		||||
@@ -34,7 +34,7 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
 | 
			
		||||
CODEOWNERS = ["@syndlex"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    from PIL import Image
 | 
			
		||||
 | 
			
		||||
    path = CORE.relative_config_path(config[CONF_FILE])
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										150
									
								
								esphome/components/anova/anova.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								esphome/components/anova/anova.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
#include "anova.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "anova";
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
 | 
			
		||||
void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); }
 | 
			
		||||
 | 
			
		||||
void Anova::setup() {
 | 
			
		||||
  this->codec_ = new AnovaCodec();
 | 
			
		||||
  this->current_request_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Anova::loop() {}
 | 
			
		||||
 | 
			
		||||
void Anova::control(const ClimateCall &call) {
 | 
			
		||||
  if (call.get_mode().has_value()) {
 | 
			
		||||
    ClimateMode mode = *call.get_mode();
 | 
			
		||||
    AnovaPacket *pkt;
 | 
			
		||||
    switch (mode) {
 | 
			
		||||
      case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
        pkt = this->codec_->get_stop_request();
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        pkt = this->codec_->get_start_request();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGW(TAG, "Unsupported mode: %d", mode);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
 | 
			
		||||
                                           pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature().has_value()) {
 | 
			
		||||
    auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature());
 | 
			
		||||
    auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
 | 
			
		||||
                                           pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->current_temperature = NAN;
 | 
			
		||||
      this->target_temperature = NAN;
 | 
			
		||||
      this->publish_state();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->char_handle_ = chr->handle;
 | 
			
		||||
 | 
			
		||||
      auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle);
 | 
			
		||||
      if (status) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      this->current_request_ = 0;
 | 
			
		||||
      this->update();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.handle != this->char_handle_)
 | 
			
		||||
        break;
 | 
			
		||||
      this->codec_->decode(param->notify.value, param->notify.value_len);
 | 
			
		||||
      if (this->codec_->has_target_temp()) {
 | 
			
		||||
        this->target_temperature = this->codec_->target_temp_;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->codec_->has_current_temp()) {
 | 
			
		||||
        this->current_temperature = this->codec_->current_temp_;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->codec_->has_running()) {
 | 
			
		||||
        this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->codec_->has_unit()) {
 | 
			
		||||
        this->fahrenheit_ = (this->codec_->unit_ == 'f');
 | 
			
		||||
        ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius");
 | 
			
		||||
        this->current_request_++;
 | 
			
		||||
      }
 | 
			
		||||
      this->publish_state();
 | 
			
		||||
 | 
			
		||||
      if (this->current_request_ > 1) {
 | 
			
		||||
        AnovaPacket *pkt = nullptr;
 | 
			
		||||
        switch (this->current_request_++) {
 | 
			
		||||
          case 2:
 | 
			
		||||
            pkt = this->codec_->get_read_target_temp_request();
 | 
			
		||||
            break;
 | 
			
		||||
          case 3:
 | 
			
		||||
            pkt = this->codec_->get_read_current_temp_request();
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            this->current_request_ = 1;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        if (pkt != nullptr) {
 | 
			
		||||
          auto status =
 | 
			
		||||
              esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length,
 | 
			
		||||
                                       pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
          if (status)
 | 
			
		||||
            ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
 | 
			
		||||
                     status);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); }
 | 
			
		||||
 | 
			
		||||
void Anova::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  if (this->current_request_ < 2) {
 | 
			
		||||
    auto pkt = this->codec_->get_read_device_status_request();
 | 
			
		||||
    if (this->current_request_ == 0)
 | 
			
		||||
      auto pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
 | 
			
		||||
    auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
 | 
			
		||||
                                           pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (status)
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    this->current_request_++;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace anova
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										52
									
								
								esphome/components/anova/anova.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/anova/anova.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "anova_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0;
 | 
			
		||||
static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1;
 | 
			
		||||
 | 
			
		||||
class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  climate::ClimateTraits traits() {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supports_heat_mode(true);
 | 
			
		||||
    traits.set_visual_min_temperature(25.0);
 | 
			
		||||
    traits.set_visual_max_temperature(100.0);
 | 
			
		||||
    traits.set_visual_temperature_step(0.1);
 | 
			
		||||
    return traits;
 | 
			
		||||
  }
 | 
			
		||||
  void set_unit_of_measurement(const char *);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AnovaCodec *codec_;
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  uint8_t current_request_;
 | 
			
		||||
  bool fahrenheit_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace anova
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										137
									
								
								esphome/components/anova/anova_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								esphome/components/anova/anova_base.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
#include "anova_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 | 
			
		||||
float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); }
 | 
			
		||||
 | 
			
		||||
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; }
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::clean_packet_() {
 | 
			
		||||
  this->packet_.length = strlen((char *) this->packet_.data);
 | 
			
		||||
  this->packet_.data[this->packet_.length] = '\0';
 | 
			
		||||
  ESP_LOGV("anova", "SendPkt: %s\n", this->packet_.data);
 | 
			
		||||
  return &this->packet_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_read_device_status_request() {
 | 
			
		||||
  this->current_query_ = READ_DEVICE_STATUS;
 | 
			
		||||
  sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
 | 
			
		||||
  this->current_query_ = READ_TARGET_TEMPERATURE;
 | 
			
		||||
  sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
 | 
			
		||||
  this->current_query_ = READ_CURRENT_TEMPERATURE;
 | 
			
		||||
  sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_read_unit_request() {
 | 
			
		||||
  this->current_query_ = READ_UNIT;
 | 
			
		||||
  sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_read_data_request() {
 | 
			
		||||
  this->current_query_ = READ_DATA;
 | 
			
		||||
  sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
 | 
			
		||||
  this->current_query_ = SET_TARGET_TEMPERATURE;
 | 
			
		||||
  if (this->fahrenheit_)
 | 
			
		||||
    temperature = ctof(temperature);
 | 
			
		||||
  sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
 | 
			
		||||
  this->current_query_ = SET_UNIT;
 | 
			
		||||
  sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_start_request() {
 | 
			
		||||
  this->current_query_ = START;
 | 
			
		||||
  sprintf((char *) this->packet_.data, CMD_START);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnovaPacket *AnovaCodec::get_stop_request() {
 | 
			
		||||
  this->current_query_ = STOP;
 | 
			
		||||
  sprintf((char *) this->packet_.data, CMD_STOP);
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
 | 
			
		||||
  memset(this->buf_, 0, 32);
 | 
			
		||||
  strncpy(this->buf_, (char *) data, length);
 | 
			
		||||
  this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false;
 | 
			
		||||
  switch (this->current_query_) {
 | 
			
		||||
    case READ_DEVICE_STATUS: {
 | 
			
		||||
      if (!strncmp(this->buf_, "stopped", 7)) {
 | 
			
		||||
        this->has_running_ = true;
 | 
			
		||||
        this->running_ = false;
 | 
			
		||||
      }
 | 
			
		||||
      if (!strncmp(this->buf_, "running", 7)) {
 | 
			
		||||
        this->has_running_ = true;
 | 
			
		||||
        this->running_ = true;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case START: {
 | 
			
		||||
      if (!strncmp(this->buf_, "start", 5)) {
 | 
			
		||||
        this->has_running_ = true;
 | 
			
		||||
        this->running_ = true;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case STOP: {
 | 
			
		||||
      if (!strncmp(this->buf_, "stop", 4)) {
 | 
			
		||||
        this->has_running_ = true;
 | 
			
		||||
        this->running_ = false;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case READ_TARGET_TEMPERATURE: {
 | 
			
		||||
      this->target_temp_ = strtof(this->buf_, nullptr);
 | 
			
		||||
      if (this->fahrenheit_)
 | 
			
		||||
        this->target_temp_ = ftoc(this->target_temp_);
 | 
			
		||||
      this->has_target_temp_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case SET_TARGET_TEMPERATURE: {
 | 
			
		||||
      this->target_temp_ = strtof(this->buf_, nullptr);
 | 
			
		||||
      if (this->fahrenheit_)
 | 
			
		||||
        this->target_temp_ = ftoc(this->target_temp_);
 | 
			
		||||
      this->has_target_temp_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case READ_CURRENT_TEMPERATURE: {
 | 
			
		||||
      this->current_temp_ = strtof(this->buf_, nullptr);
 | 
			
		||||
      if (this->fahrenheit_)
 | 
			
		||||
        this->current_temp_ = ftoc(this->current_temp_);
 | 
			
		||||
      this->has_current_temp_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case SET_UNIT:
 | 
			
		||||
    case READ_UNIT: {
 | 
			
		||||
      this->unit_ = this->buf_[0];
 | 
			
		||||
      this->fahrenheit_ = this->buf_[0] == 'f';
 | 
			
		||||
      this->has_unit_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace anova
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										80
									
								
								esphome/components/anova/anova_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/anova/anova_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 | 
			
		||||
enum CurrentQuery {
 | 
			
		||||
  NONE,
 | 
			
		||||
  READ_DEVICE_STATUS,
 | 
			
		||||
  READ_TARGET_TEMPERATURE,
 | 
			
		||||
  READ_CURRENT_TEMPERATURE,
 | 
			
		||||
  READ_DATA,
 | 
			
		||||
  READ_UNIT,
 | 
			
		||||
  SET_TARGET_TEMPERATURE,
 | 
			
		||||
  SET_UNIT,
 | 
			
		||||
  START,
 | 
			
		||||
  STOP,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AnovaPacket {
 | 
			
		||||
  uint16_t length;
 | 
			
		||||
  uint8_t data[24];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#define CMD_READ_DEVICE_STATUS "status\r"
 | 
			
		||||
#define CMD_READ_TARGET_TEMP "read set temp\r"
 | 
			
		||||
#define CMD_READ_CURRENT_TEMP "read temp\r"
 | 
			
		||||
#define CMD_READ_UNIT "read unit\r"
 | 
			
		||||
#define CMD_READ_DATA "read data\r"
 | 
			
		||||
#define CMD_SET_TARGET_TEMP "set temp %.1f\r"
 | 
			
		||||
#define CMD_SET_TEMP_UNIT "set unit %c\r"
 | 
			
		||||
 | 
			
		||||
#define CMD_START "start\r"
 | 
			
		||||
#define CMD_STOP "stop\r"
 | 
			
		||||
 | 
			
		||||
class AnovaCodec {
 | 
			
		||||
 public:
 | 
			
		||||
  AnovaPacket *get_read_device_status_request();
 | 
			
		||||
  AnovaPacket *get_read_target_temp_request();
 | 
			
		||||
  AnovaPacket *get_read_current_temp_request();
 | 
			
		||||
  AnovaPacket *get_read_data_request();
 | 
			
		||||
  AnovaPacket *get_read_unit_request();
 | 
			
		||||
 | 
			
		||||
  AnovaPacket *get_set_target_temp_request(float temperature);
 | 
			
		||||
  AnovaPacket *get_set_unit_request(char unit);
 | 
			
		||||
 | 
			
		||||
  AnovaPacket *get_start_request();
 | 
			
		||||
  AnovaPacket *get_stop_request();
 | 
			
		||||
 | 
			
		||||
  void decode(const uint8_t *data, uint16_t length);
 | 
			
		||||
  bool has_target_temp() { return this->has_target_temp_; }
 | 
			
		||||
  bool has_current_temp() { return this->has_current_temp_; }
 | 
			
		||||
  bool has_unit() { return this->has_unit_; }
 | 
			
		||||
  bool has_running() { return this->has_running_; }
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    float target_temp_;
 | 
			
		||||
    float current_temp_;
 | 
			
		||||
    char unit_;
 | 
			
		||||
    bool running_;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AnovaPacket *clean_packet_();
 | 
			
		||||
  AnovaPacket packet_;
 | 
			
		||||
 | 
			
		||||
  bool has_target_temp_;
 | 
			
		||||
  bool has_current_temp_;
 | 
			
		||||
  bool has_unit_;
 | 
			
		||||
  bool has_running_;
 | 
			
		||||
  char buf_[32];
 | 
			
		||||
  bool fahrenheit_;
 | 
			
		||||
 | 
			
		||||
  CurrentQuery current_query_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace anova
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										36
									
								
								esphome/components/anova/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/anova/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import climate, ble_client
 | 
			
		||||
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT
 | 
			
		||||
 | 
			
		||||
UNITS = {
 | 
			
		||||
    "f": "f",
 | 
			
		||||
    "c": "c",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
anova_ns = cg.esphome_ns.namespace("anova")
 | 
			
		||||
Anova = anova_ns.class_(
 | 
			
		||||
    "Anova", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Anova),
 | 
			
		||||
            cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))
 | 
			
		||||
@@ -23,7 +23,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    yield i2c.register_i2c_device(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,10 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace apds9960 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "apds9960";
 | 
			
		||||
static const char *const TAG = "apds9960";
 | 
			
		||||
 | 
			
		||||
#define APDS9960_ERROR_CHECK(func) \
 | 
			
		||||
  if (!func) { \
 | 
			
		||||
  if (!(func)) { \
 | 
			
		||||
    this->mark_failed(); \
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    hub = yield cg.get_variable(config[CONF_APDS9960_ID])
 | 
			
		||||
    var = yield binary_sensor.new_binary_sensor(config)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    hub = await cg.get_variable(config[CONF_APDS9960_ID])
 | 
			
		||||
    var = await binary_sensor.new_binary_sensor(config)
 | 
			
		||||
    func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]])
 | 
			
		||||
    cg.add(func(var))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,12 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import CONF_TYPE, DEVICE_CLASS_EMPTY, UNIT_PERCENT, ICON_LIGHTBULB
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
)
 | 
			
		||||
from . import APDS9960, CONF_APDS9960_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["apds9960"]
 | 
			
		||||
@@ -15,7 +20,10 @@ TYPES = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = sensor.sensor_schema(
 | 
			
		||||
    UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY
 | 
			
		||||
    unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
    icon=ICON_LIGHTBULB,
 | 
			
		||||
    accuracy_decimals=1,
 | 
			
		||||
    state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True),
 | 
			
		||||
@@ -24,8 +32,8 @@ CONFIG_SCHEMA = sensor.sensor_schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    hub = yield cg.get_variable(config[CONF_APDS9960_ID])
 | 
			
		||||
    var = yield sensor.new_sensor(config)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    hub = await cg.get_variable(config[CONF_APDS9960_ID])
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    func = getattr(hub, TYPES[config[CONF_TYPE]])
 | 
			
		||||
    cg.add(func(var))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
@@ -6,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_DATA,
 | 
			
		||||
    CONF_DATA_TEMPLATE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_KEY,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_REBOOT_TIMEOUT,
 | 
			
		||||
@@ -19,7 +22,7 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["async_tcp"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
api_ns = cg.esphome_ns.namespace("api")
 | 
			
		||||
@@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = {
 | 
			
		||||
    "float[]": cg.std_vector.template(float),
 | 
			
		||||
    "string[]": cg.std_vector.template(cg.std_string),
 | 
			
		||||
}
 | 
			
		||||
CONF_ENCRYPTION = "encryption"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_encryption_key(value):
 | 
			
		||||
    value = cv.string_strict(value)
 | 
			
		||||
    try:
 | 
			
		||||
        decoded = base64.b64decode(value, validate=True)
 | 
			
		||||
    except ValueError as err:
 | 
			
		||||
        raise cv.Invalid("Invalid key format, please check it's using base64") from err
 | 
			
		||||
 | 
			
		||||
    if len(decoded) != 32:
 | 
			
		||||
        raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
 | 
			
		||||
 | 
			
		||||
    # Return original data for roundtrip conversion
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -63,14 +82,19 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ENCRYPTION): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_KEY): validate_encryption_key,
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(40.0)
 | 
			
		||||
def to_code(config):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield cg.register_component(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_port(config[CONF_PORT]))
 | 
			
		||||
    cg.add(var.set_password(config[CONF_PASSWORD]))
 | 
			
		||||
@@ -90,7 +114,16 @@ def to_code(config):
 | 
			
		||||
            conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.register_user_service(trigger))
 | 
			
		||||
        yield automation.build_automation(trigger, func_args, conf)
 | 
			
		||||
        await automation.build_automation(trigger, func_args, conf)
 | 
			
		||||
 | 
			
		||||
    if CONF_ENCRYPTION in config:
 | 
			
		||||
        conf = config[CONF_ENCRYPTION]
 | 
			
		||||
        decoded = base64.b64decode(conf[CONF_KEY])
 | 
			
		||||
        cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
        cg.add_define("USE_API_NOISE")
 | 
			
		||||
        cg.add_library("esphome/noise-c", "0.1.1")
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add_define("USE_API_PLAINTEXT")
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_API")
 | 
			
		||||
    cg.add_global(api_ns.using)
 | 
			
		||||
@@ -116,21 +149,21 @@ HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_SERVICE_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = yield cg.get_variable(config[CONF_ID])
 | 
			
		||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, False)
 | 
			
		||||
    templ = yield cg.templatable(config[CONF_SERVICE], args, None)
 | 
			
		||||
    templ = await cg.templatable(config[CONF_SERVICE], args, None)
 | 
			
		||||
    cg.add(var.set_service(templ))
 | 
			
		||||
    for key, value in config[CONF_DATA].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_data(key, templ))
 | 
			
		||||
    for key, value in config[CONF_DATA_TEMPLATE].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_data_template(key, templ))
 | 
			
		||||
    for key, value in config[CONF_VARIABLES].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_variable(key, templ))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_homeassistant_event(value):
 | 
			
		||||
@@ -159,21 +192,21 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_EVENT_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
def homeassistant_event_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = yield cg.get_variable(config[CONF_ID])
 | 
			
		||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, True)
 | 
			
		||||
    templ = yield cg.templatable(config[CONF_EVENT], args, None)
 | 
			
		||||
    templ = await cg.templatable(config[CONF_EVENT], args, None)
 | 
			
		||||
    cg.add(var.set_service(templ))
 | 
			
		||||
    for key, value in config[CONF_DATA].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_data(key, templ))
 | 
			
		||||
    for key, value in config[CONF_DATA_TEMPLATE].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_data_template(key, templ))
 | 
			
		||||
    for key, value in config[CONF_VARIABLES].items():
 | 
			
		||||
        templ = yield cg.templatable(value, args, None)
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_variable(key, templ))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
 | 
			
		||||
@@ -190,15 +223,15 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = yield cg.get_variable(config[CONF_ID])
 | 
			
		||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, True)
 | 
			
		||||
    cg.add(var.set_service("esphome.tag_scanned"))
 | 
			
		||||
    templ = yield cg.templatable(config[CONF_TAG], args, cg.std_string)
 | 
			
		||||
    templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
 | 
			
		||||
    cg.add(var.add_data("tag_id", templ))
 | 
			
		||||
    yield var
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
 | 
			
		||||
def api_connected_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    yield cg.new_Pvariable(condition_id, template_arg)
 | 
			
		||||
async def api_connected_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,8 @@ service APIConnection {
 | 
			
		||||
  rpc switch_command (SwitchCommandRequest) returns (void) {}
 | 
			
		||||
  rpc camera_image (CameraImageRequest) returns (void) {}
 | 
			
		||||
  rpc climate_command (ClimateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc number_command (NumberCommandRequest) returns (void) {}
 | 
			
		||||
  rpc select_command (SelectCommandRequest) returns (void) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -176,6 +178,10 @@ message DeviceInfoResponse {
 | 
			
		||||
  string model = 6;
 | 
			
		||||
 | 
			
		||||
  bool has_deep_sleep = 7;
 | 
			
		||||
 | 
			
		||||
  // The esphome project details if set
 | 
			
		||||
  string project_name = 8;
 | 
			
		||||
  string project_version = 9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListEntitiesRequest {
 | 
			
		||||
@@ -208,6 +214,7 @@ message ListEntitiesBinarySensorResponse {
 | 
			
		||||
 | 
			
		||||
  string device_class = 5;
 | 
			
		||||
  bool is_status_binary_sensor = 6;
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
}
 | 
			
		||||
message BinarySensorStateResponse {
 | 
			
		||||
  option (id) = 21;
 | 
			
		||||
@@ -237,6 +244,7 @@ message ListEntitiesCoverResponse {
 | 
			
		||||
  bool supports_position = 6;
 | 
			
		||||
  bool supports_tilt = 7;
 | 
			
		||||
  string device_class = 8;
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum LegacyCoverState {
 | 
			
		||||
@@ -304,6 +312,7 @@ message ListEntitiesFanResponse {
 | 
			
		||||
  bool supports_speed = 6;
 | 
			
		||||
  bool supports_direction = 7;
 | 
			
		||||
  int32 supported_speed_count = 8;
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
}
 | 
			
		||||
enum FanSpeed {
 | 
			
		||||
  FAN_SPEED_LOW = 0;
 | 
			
		||||
@@ -347,6 +356,18 @@ message FanCommandRequest {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== LIGHT ====================
 | 
			
		||||
enum ColorMode {
 | 
			
		||||
  COLOR_MODE_UNKNOWN = 0;
 | 
			
		||||
  COLOR_MODE_ON_OFF = 1;
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 2;
 | 
			
		||||
  COLOR_MODE_WHITE = 7;
 | 
			
		||||
  COLOR_MODE_COLOR_TEMPERATURE = 11;
 | 
			
		||||
  COLOR_MODE_COLD_WARM_WHITE = 19;
 | 
			
		||||
  COLOR_MODE_RGB = 35;
 | 
			
		||||
  COLOR_MODE_RGB_WHITE = 39;
 | 
			
		||||
  COLOR_MODE_RGB_COLOR_TEMPERATURE = 47;
 | 
			
		||||
  COLOR_MODE_RGB_COLD_WARM_WHITE = 51;
 | 
			
		||||
}
 | 
			
		||||
message ListEntitiesLightResponse {
 | 
			
		||||
  option (id) = 15;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
@@ -357,13 +378,16 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  bool supports_brightness = 5;
 | 
			
		||||
  bool supports_rgb = 6;
 | 
			
		||||
  bool supports_white_value = 7;
 | 
			
		||||
  bool supports_color_temperature = 8;
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12;
 | 
			
		||||
  // next four supports_* are for legacy clients, newer clients should use color modes
 | 
			
		||||
  bool legacy_supports_brightness = 5 [deprecated=true];
 | 
			
		||||
  bool legacy_supports_rgb = 6 [deprecated=true];
 | 
			
		||||
  bool legacy_supports_white_value = 7 [deprecated=true];
 | 
			
		||||
  bool legacy_supports_color_temperature = 8 [deprecated=true];
 | 
			
		||||
  float min_mireds = 9;
 | 
			
		||||
  float max_mireds = 10;
 | 
			
		||||
  repeated string effects = 11;
 | 
			
		||||
  bool disabled_by_default = 13;
 | 
			
		||||
}
 | 
			
		||||
message LightStateResponse {
 | 
			
		||||
  option (id) = 24;
 | 
			
		||||
@@ -374,11 +398,15 @@ message LightStateResponse {
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  bool state = 2;
 | 
			
		||||
  float brightness = 3;
 | 
			
		||||
  ColorMode color_mode = 11;
 | 
			
		||||
  float color_brightness = 10;
 | 
			
		||||
  float red = 4;
 | 
			
		||||
  float green = 5;
 | 
			
		||||
  float blue = 6;
 | 
			
		||||
  float white = 7;
 | 
			
		||||
  float color_temperature = 8;
 | 
			
		||||
  float cold_white = 12;
 | 
			
		||||
  float warm_white = 13;
 | 
			
		||||
  string effect = 9;
 | 
			
		||||
}
 | 
			
		||||
message LightCommandRequest {
 | 
			
		||||
@@ -392,6 +420,10 @@ message LightCommandRequest {
 | 
			
		||||
  bool state = 3;
 | 
			
		||||
  bool has_brightness = 4;
 | 
			
		||||
  float brightness = 5;
 | 
			
		||||
  bool has_color_mode = 22;
 | 
			
		||||
  ColorMode color_mode = 23;
 | 
			
		||||
  bool has_color_brightness = 20;
 | 
			
		||||
  float color_brightness = 21;
 | 
			
		||||
  bool has_rgb = 6;
 | 
			
		||||
  float red = 7;
 | 
			
		||||
  float green = 8;
 | 
			
		||||
@@ -400,6 +432,10 @@ message LightCommandRequest {
 | 
			
		||||
  float white = 11;
 | 
			
		||||
  bool has_color_temperature = 12;
 | 
			
		||||
  float color_temperature = 13;
 | 
			
		||||
  bool has_cold_white = 24;
 | 
			
		||||
  float cold_white = 25;
 | 
			
		||||
  bool has_warm_white = 26;
 | 
			
		||||
  float warm_white = 27;
 | 
			
		||||
  bool has_transition_length = 14;
 | 
			
		||||
  uint32 transition_length = 15;
 | 
			
		||||
  bool has_flash_length = 16;
 | 
			
		||||
@@ -409,6 +445,18 @@ message LightCommandRequest {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== SENSOR ====================
 | 
			
		||||
enum SensorStateClass {
 | 
			
		||||
  STATE_CLASS_NONE = 0;
 | 
			
		||||
  STATE_CLASS_MEASUREMENT = 1;
 | 
			
		||||
  STATE_CLASS_TOTAL_INCREASING = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum SensorLastResetType {
 | 
			
		||||
  LAST_RESET_NONE = 0;
 | 
			
		||||
  LAST_RESET_NEVER = 1;
 | 
			
		||||
  LAST_RESET_AUTO = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListEntitiesSensorResponse {
 | 
			
		||||
  option (id) = 16;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
@@ -424,6 +472,10 @@ message ListEntitiesSensorResponse {
 | 
			
		||||
  int32 accuracy_decimals = 7;
 | 
			
		||||
  bool force_update = 8;
 | 
			
		||||
  string device_class = 9;
 | 
			
		||||
  SensorStateClass state_class = 10;
 | 
			
		||||
  // Last reset type removed in 2021.9.0
 | 
			
		||||
  SensorLastResetType legacy_last_reset_type = 11;
 | 
			
		||||
  bool disabled_by_default = 12;
 | 
			
		||||
}
 | 
			
		||||
message SensorStateResponse {
 | 
			
		||||
  option (id) = 25;
 | 
			
		||||
@@ -451,6 +503,7 @@ message ListEntitiesSwitchResponse {
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool assumed_state = 6;
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
}
 | 
			
		||||
message SwitchStateResponse {
 | 
			
		||||
  option (id) = 26;
 | 
			
		||||
@@ -483,6 +536,7 @@ message ListEntitiesTextSensorResponse {
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
}
 | 
			
		||||
message TextSensorStateResponse {
 | 
			
		||||
  option (id) = 27;
 | 
			
		||||
@@ -503,9 +557,10 @@ enum LogLevel {
 | 
			
		||||
  LOG_LEVEL_ERROR = 1;
 | 
			
		||||
  LOG_LEVEL_WARN = 2;
 | 
			
		||||
  LOG_LEVEL_INFO = 3;
 | 
			
		||||
  LOG_LEVEL_DEBUG = 4;
 | 
			
		||||
  LOG_LEVEL_VERBOSE = 5;
 | 
			
		||||
  LOG_LEVEL_VERY_VERBOSE = 6;
 | 
			
		||||
  LOG_LEVEL_CONFIG = 4;
 | 
			
		||||
  LOG_LEVEL_DEBUG = 5;
 | 
			
		||||
  LOG_LEVEL_VERBOSE = 6;
 | 
			
		||||
  LOG_LEVEL_VERY_VERBOSE = 7;
 | 
			
		||||
}
 | 
			
		||||
message SubscribeLogsRequest {
 | 
			
		||||
  option (id) = 28;
 | 
			
		||||
@@ -520,7 +575,6 @@ message SubscribeLogsResponse {
 | 
			
		||||
  option (no_delay) = false;
 | 
			
		||||
 | 
			
		||||
  LogLevel level = 1;
 | 
			
		||||
  string tag = 2;
 | 
			
		||||
  string message = 3;
 | 
			
		||||
  bool send_failed = 4;
 | 
			
		||||
}
 | 
			
		||||
@@ -561,6 +615,7 @@ message SubscribeHomeAssistantStateResponse {
 | 
			
		||||
  option (id) = 39;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  string entity_id = 1;
 | 
			
		||||
  string attribute = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message HomeAssistantStateResponse {
 | 
			
		||||
@@ -570,6 +625,7 @@ message HomeAssistantStateResponse {
 | 
			
		||||
 | 
			
		||||
  string entity_id = 1;
 | 
			
		||||
  string state = 2;
 | 
			
		||||
  string attribute = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== IMPORT TIME ====================
 | 
			
		||||
@@ -640,6 +696,7 @@ message ListEntitiesCameraResponse {
 | 
			
		||||
  fixed32 key = 2;
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
  bool disabled_by_default = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message CameraImageResponse {
 | 
			
		||||
@@ -664,11 +721,12 @@ message CameraImageRequest {
 | 
			
		||||
// ==================== CLIMATE ====================
 | 
			
		||||
enum ClimateMode {
 | 
			
		||||
  CLIMATE_MODE_OFF = 0;
 | 
			
		||||
  CLIMATE_MODE_AUTO = 1;
 | 
			
		||||
  CLIMATE_MODE_HEAT_COOL = 1;
 | 
			
		||||
  CLIMATE_MODE_COOL = 2;
 | 
			
		||||
  CLIMATE_MODE_HEAT = 3;
 | 
			
		||||
  CLIMATE_MODE_FAN_ONLY = 4;
 | 
			
		||||
  CLIMATE_MODE_DRY = 5;
 | 
			
		||||
  CLIMATE_MODE_AUTO = 6;
 | 
			
		||||
}
 | 
			
		||||
enum ClimateFanMode {
 | 
			
		||||
  CLIMATE_FAN_ON = 0;
 | 
			
		||||
@@ -696,6 +754,16 @@ enum ClimateAction {
 | 
			
		||||
  CLIMATE_ACTION_DRYING = 5;
 | 
			
		||||
  CLIMATE_ACTION_FAN = 6;
 | 
			
		||||
}
 | 
			
		||||
enum ClimatePreset {
 | 
			
		||||
  CLIMATE_PRESET_NONE = 0;
 | 
			
		||||
  CLIMATE_PRESET_HOME = 1;
 | 
			
		||||
  CLIMATE_PRESET_AWAY = 2;
 | 
			
		||||
  CLIMATE_PRESET_BOOST = 3;
 | 
			
		||||
  CLIMATE_PRESET_COMFORT = 4;
 | 
			
		||||
  CLIMATE_PRESET_ECO = 5;
 | 
			
		||||
  CLIMATE_PRESET_SLEEP = 6;
 | 
			
		||||
  CLIMATE_PRESET_ACTIVITY = 7;
 | 
			
		||||
}
 | 
			
		||||
message ListEntitiesClimateResponse {
 | 
			
		||||
  option (id) = 46;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
@@ -712,10 +780,16 @@ message ListEntitiesClimateResponse {
 | 
			
		||||
  float visual_min_temperature = 8;
 | 
			
		||||
  float visual_max_temperature = 9;
 | 
			
		||||
  float visual_temperature_step = 10;
 | 
			
		||||
  bool supports_away = 11;
 | 
			
		||||
  // for older peer versions - in new system this
 | 
			
		||||
  // is if CLIMATE_PRESET_AWAY exists is supported_presets
 | 
			
		||||
  bool legacy_supports_away = 11;
 | 
			
		||||
  bool supports_action = 12;
 | 
			
		||||
  repeated ClimateFanMode supported_fan_modes = 13;
 | 
			
		||||
  repeated ClimateSwingMode supported_swing_modes = 14;
 | 
			
		||||
  repeated string supported_custom_fan_modes = 15;
 | 
			
		||||
  repeated ClimatePreset supported_presets = 16;
 | 
			
		||||
  repeated string supported_custom_presets = 17;
 | 
			
		||||
  bool disabled_by_default = 18;
 | 
			
		||||
}
 | 
			
		||||
message ClimateStateResponse {
 | 
			
		||||
  option (id) = 47;
 | 
			
		||||
@@ -729,10 +803,14 @@ message ClimateStateResponse {
 | 
			
		||||
  float target_temperature = 4;
 | 
			
		||||
  float target_temperature_low = 5;
 | 
			
		||||
  float target_temperature_high = 6;
 | 
			
		||||
  bool away = 7;
 | 
			
		||||
  // For older peers, equal to preset == CLIMATE_PRESET_AWAY
 | 
			
		||||
  bool legacy_away = 7;
 | 
			
		||||
  ClimateAction action = 8;
 | 
			
		||||
  ClimateFanMode fan_mode = 9;
 | 
			
		||||
  ClimateSwingMode swing_mode = 10;
 | 
			
		||||
  string custom_fan_mode = 11;
 | 
			
		||||
  ClimatePreset preset = 12;
 | 
			
		||||
  string custom_preset = 13;
 | 
			
		||||
}
 | 
			
		||||
message ClimateCommandRequest {
 | 
			
		||||
  option (id) = 48;
 | 
			
		||||
@@ -749,10 +827,93 @@ message ClimateCommandRequest {
 | 
			
		||||
  float target_temperature_low = 7;
 | 
			
		||||
  bool has_target_temperature_high = 8;
 | 
			
		||||
  float target_temperature_high = 9;
 | 
			
		||||
  bool has_away = 10;
 | 
			
		||||
  bool away = 11;
 | 
			
		||||
  // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
 | 
			
		||||
  bool has_legacy_away = 10;
 | 
			
		||||
  bool legacy_away = 11;
 | 
			
		||||
  bool has_fan_mode = 12;
 | 
			
		||||
  ClimateFanMode fan_mode = 13;
 | 
			
		||||
  bool has_swing_mode = 14;
 | 
			
		||||
  ClimateSwingMode swing_mode = 15;
 | 
			
		||||
  bool has_custom_fan_mode = 16;
 | 
			
		||||
  string custom_fan_mode = 17;
 | 
			
		||||
  bool has_preset = 18;
 | 
			
		||||
  ClimatePreset preset = 19;
 | 
			
		||||
  bool has_custom_preset = 20;
 | 
			
		||||
  string custom_preset = 21;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== NUMBER ====================
 | 
			
		||||
message ListEntitiesNumberResponse {
 | 
			
		||||
  option (id) = 49;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_NUMBER";
 | 
			
		||||
 | 
			
		||||
  string object_id = 1;
 | 
			
		||||
  fixed32 key = 2;
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  float min_value = 6;
 | 
			
		||||
  float max_value = 7;
 | 
			
		||||
  float step = 8;
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
}
 | 
			
		||||
message NumberStateResponse {
 | 
			
		||||
  option (id) = 50;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_NUMBER";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  float state = 2;
 | 
			
		||||
  // If the number does not have a valid state yet.
 | 
			
		||||
  // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
 | 
			
		||||
  bool missing_state = 3;
 | 
			
		||||
}
 | 
			
		||||
message NumberCommandRequest {
 | 
			
		||||
  option (id) = 51;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_NUMBER";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  float state = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== SELECT ====================
 | 
			
		||||
message ListEntitiesSelectResponse {
 | 
			
		||||
  option (id) = 52;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_SELECT";
 | 
			
		||||
 | 
			
		||||
  string object_id = 1;
 | 
			
		||||
  fixed32 key = 2;
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  repeated string options = 6;
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
}
 | 
			
		||||
message SelectStateResponse {
 | 
			
		||||
  option (id) = 53;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_SELECT";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  string state = 2;
 | 
			
		||||
  // If the select does not have a valid state yet.
 | 
			
		||||
  // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
 | 
			
		||||
  bool missing_state = 3;
 | 
			
		||||
}
 | 
			
		||||
message SelectCommandRequest {
 | 
			
		||||
  option (id) = 54;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_SELECT";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  string state = 2;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
 | 
			
		||||
@@ -16,147 +17,148 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "api.connection";
 | 
			
		||||
static const char *const TAG = "api.connection";
 | 
			
		||||
 | 
			
		||||
APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
 | 
			
		||||
    : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
 | 
			
		||||
  this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
 | 
			
		||||
  this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
 | 
			
		||||
  this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
 | 
			
		||||
                           this);
 | 
			
		||||
  this->client_->onData([](void *s, AsyncClient *c, void *buf,
 | 
			
		||||
                           size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
 | 
			
		||||
                        this);
 | 
			
		||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
 | 
			
		||||
  this->proto_write_buffer_.reserve(64);
 | 
			
		||||
 | 
			
		||||
  this->send_buffer_.reserve(64);
 | 
			
		||||
  this->recv_buffer_.reserve(32);
 | 
			
		||||
  this->client_info_ = this->client_->remoteIP().toString().c_str();
 | 
			
		||||
#if defined(USE_API_PLAINTEXT)
 | 
			
		||||
  helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
 | 
			
		||||
#elif defined(USE_API_NOISE)
 | 
			
		||||
  helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
 | 
			
		||||
#else
 | 
			
		||||
#error "No frame helper defined"
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::start() {
 | 
			
		||||
  this->last_traffic_ = millis();
 | 
			
		||||
}
 | 
			
		||||
APIConnection::~APIConnection() { delete this->client_; }
 | 
			
		||||
void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
 | 
			
		||||
void APIConnection::on_disconnect_() { this->remove_ = true; }
 | 
			
		||||
void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
 | 
			
		||||
void APIConnection::on_data_(uint8_t *buf, size_t len) {
 | 
			
		||||
  if (len == 0 || buf == nullptr)
 | 
			
		||||
 | 
			
		||||
  APIError err = helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::parse_recv_buffer_() {
 | 
			
		||||
  if (this->recv_buffer_.empty() || this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  while (!this->recv_buffer_.empty()) {
 | 
			
		||||
    if (this->recv_buffer_[0] != 0x00) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    uint32_t i = 1;
 | 
			
		||||
    const uint32_t size = this->recv_buffer_.size();
 | 
			
		||||
    uint32_t consumed;
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
 | 
			
		||||
    if (!msg_size_varint.has_value())
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      return;
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    uint32_t msg_size = msg_size_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value())
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      return;
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    uint32_t msg_type = msg_type_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    if (size - i < msg_size)
 | 
			
		||||
      // message body not fully received
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    uint8_t *msg = &this->recv_buffer_[i];
 | 
			
		||||
    this->read_message(msg_size, msg_type, msg);
 | 
			
		||||
    if (this->remove_)
 | 
			
		||||
      return;
 | 
			
		||||
    // pop front
 | 
			
		||||
    uint32_t total = i + msg_size;
 | 
			
		||||
    this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
 | 
			
		||||
    this->last_traffic_ = millis();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::disconnect_client() {
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->remove_ = true;
 | 
			
		||||
  client_info_ = helper_->getpeername();
 | 
			
		||||
  helper_->set_log_info(client_info_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::loop() {
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  if (this->next_close_) {
 | 
			
		||||
    this->disconnect_client();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!network_is_connected()) {
 | 
			
		||||
    // when network is disconnected force disconnect immediately
 | 
			
		||||
    // don't wait for timeout
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->client_->disconnected()) {
 | 
			
		||||
    // failsafe for disconnect logic
 | 
			
		||||
    this->on_disconnect_();
 | 
			
		||||
  if (this->next_close_) {
 | 
			
		||||
    // requested a disconnect
 | 
			
		||||
    this->helper_->close();
 | 
			
		||||
    this->remove_ = true;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->parse_recv_buffer_();
 | 
			
		||||
 | 
			
		||||
  APIError err = helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ReadPacketBuffer buffer;
 | 
			
		||||
  err = helper_->read_packet(&buffer);
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    // pass
 | 
			
		||||
  } else if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->last_traffic_ = millis();
 | 
			
		||||
    // read a packet
 | 
			
		||||
    this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
    if (this->remove_)
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->list_entities_iterator_.advance();
 | 
			
		||||
  this->initial_state_iterator_.advance();
 | 
			
		||||
 | 
			
		||||
  const uint32_t keepalive = 60000;
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  if (this->sent_ping_) {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
 | 
			
		||||
      this->disconnect_client();
 | 
			
		||||
    if (now - this->last_traffic_ > (keepalive * 5) / 2) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (millis() - this->last_traffic_ > keepalive) {
 | 
			
		||||
  } else if (now - this->last_traffic_ > keepalive) {
 | 
			
		||||
    this->sent_ping_ = true;
 | 
			
		||||
    this->send_ping_request(PingRequest());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  if (this->image_reader_.available()) {
 | 
			
		||||
    uint32_t space = this->client_->space();
 | 
			
		||||
    // reserve 15 bytes for metadata, and at least 64 bytes of data
 | 
			
		||||
    if (space >= 15 + 64) {
 | 
			
		||||
      uint32_t to_send = std::min(space - 15, this->image_reader_.available());
 | 
			
		||||
      auto buffer = this->create_buffer();
 | 
			
		||||
      // fixed32 key = 1;
 | 
			
		||||
      buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
 | 
			
		||||
      // bytes data = 2;
 | 
			
		||||
      buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
 | 
			
		||||
      // bool done = 3;
 | 
			
		||||
      bool done = this->image_reader_.available() == to_send;
 | 
			
		||||
      buffer.encode_bool(3, done);
 | 
			
		||||
      bool success = this->send_buffer(buffer, 44);
 | 
			
		||||
  if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
 | 
			
		||||
    uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
 | 
			
		||||
    auto buffer = this->create_buffer();
 | 
			
		||||
    // fixed32 key = 1;
 | 
			
		||||
    buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
 | 
			
		||||
    // bytes data = 2;
 | 
			
		||||
    buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
 | 
			
		||||
    // bool done = 3;
 | 
			
		||||
    bool done = this->image_reader_.available() == to_send;
 | 
			
		||||
    buffer.encode_bool(3, done);
 | 
			
		||||
    bool success = this->send_buffer(buffer, 44);
 | 
			
		||||
 | 
			
		||||
      if (success) {
 | 
			
		||||
        this->image_reader_.consume_data(to_send);
 | 
			
		||||
      }
 | 
			
		||||
      if (success && done) {
 | 
			
		||||
        this->image_reader_.return_image();
 | 
			
		||||
      }
 | 
			
		||||
    if (success) {
 | 
			
		||||
      this->image_reader_.consume_data(to_send);
 | 
			
		||||
    }
 | 
			
		||||
    if (success && done) {
 | 
			
		||||
      this->image_reader_.return_image();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (state_subs_at_ != -1) {
 | 
			
		||||
    const auto &subs = this->parent_->get_state_subs();
 | 
			
		||||
    if (state_subs_at_ >= subs.size()) {
 | 
			
		||||
      state_subs_at_ = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
      auto &it = subs[state_subs_at_];
 | 
			
		||||
      SubscribeHomeAssistantStateResponse resp;
 | 
			
		||||
      resp.entity_id = it.entity_id;
 | 
			
		||||
      resp.attribute = it.attribute.value();
 | 
			
		||||
      if (this->send_subscribe_home_assistant_state_response(resp)) {
 | 
			
		||||
        state_subs_at_++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
 | 
			
		||||
  return App.get_name() + component_type + nameable->get_object_id();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DisconnectResponse APIConnection::disconnect(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 requested disconnected", client_info_.c_str());
 | 
			
		||||
  this->next_close_ = true;
 | 
			
		||||
  DisconnectResponse resp;
 | 
			
		||||
  return resp;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
 | 
			
		||||
  // pass
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
@@ -176,6 +178,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_
 | 
			
		||||
  msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor);
 | 
			
		||||
  msg.device_class = binary_sensor->get_device_class();
 | 
			
		||||
  msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
 | 
			
		||||
  msg.disabled_by_default = binary_sensor->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_binary_sensor_response(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -207,6 +210,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) {
 | 
			
		||||
  msg.supports_position = traits.get_supports_position();
 | 
			
		||||
  msg.supports_tilt = traits.get_supports_tilt();
 | 
			
		||||
  msg.device_class = cover->get_device_class();
 | 
			
		||||
  msg.disabled_by_default = cover->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_cover_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
@@ -239,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
bool APIConnection::send_fan_state(fan::FanState *fan) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -268,6 +275,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) {
 | 
			
		||||
  msg.supports_speed = traits.supports_speed();
 | 
			
		||||
  msg.supports_direction = traits.supports_direction();
 | 
			
		||||
  msg.supported_speed_count = traits.supported_speed_count();
 | 
			
		||||
  msg.disabled_by_default = fan->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_fan_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
@@ -292,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
@@ -301,21 +310,21 @@ bool APIConnection::send_light_state(light::LightState *light) {
 | 
			
		||||
 | 
			
		||||
  auto traits = light->get_traits();
 | 
			
		||||
  auto values = light->remote_values;
 | 
			
		||||
  auto color_mode = values.get_color_mode();
 | 
			
		||||
  LightStateResponse resp{};
 | 
			
		||||
 | 
			
		||||
  resp.key = light->get_object_id_hash();
 | 
			
		||||
  resp.state = values.is_on();
 | 
			
		||||
  if (traits.get_supports_brightness())
 | 
			
		||||
    resp.brightness = values.get_brightness();
 | 
			
		||||
  if (traits.get_supports_rgb()) {
 | 
			
		||||
    resp.red = values.get_red();
 | 
			
		||||
    resp.green = values.get_green();
 | 
			
		||||
    resp.blue = values.get_blue();
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_rgb_white_value())
 | 
			
		||||
    resp.white = values.get_white();
 | 
			
		||||
  if (traits.get_supports_color_temperature())
 | 
			
		||||
    resp.color_temperature = values.get_color_temperature();
 | 
			
		||||
  resp.color_mode = static_cast<enums::ColorMode>(color_mode);
 | 
			
		||||
  resp.brightness = values.get_brightness();
 | 
			
		||||
  resp.color_brightness = values.get_color_brightness();
 | 
			
		||||
  resp.red = values.get_red();
 | 
			
		||||
  resp.green = values.get_green();
 | 
			
		||||
  resp.blue = values.get_blue();
 | 
			
		||||
  resp.white = values.get_white();
 | 
			
		||||
  resp.color_temperature = values.get_color_temperature();
 | 
			
		||||
  resp.cold_white = values.get_cold_white();
 | 
			
		||||
  resp.warm_white = values.get_warm_white();
 | 
			
		||||
  if (light->supports_effects())
 | 
			
		||||
    resp.effect = light->get_effect_name();
 | 
			
		||||
  return this->send_light_state_response(resp);
 | 
			
		||||
@@ -327,11 +336,21 @@ bool APIConnection::send_light_info(light::LightState *light) {
 | 
			
		||||
  msg.object_id = light->get_object_id();
 | 
			
		||||
  msg.name = light->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("light", light);
 | 
			
		||||
  msg.supports_brightness = traits.get_supports_brightness();
 | 
			
		||||
  msg.supports_rgb = traits.get_supports_rgb();
 | 
			
		||||
  msg.supports_white_value = traits.get_supports_rgb_white_value();
 | 
			
		||||
  msg.supports_color_temperature = traits.get_supports_color_temperature();
 | 
			
		||||
  if (msg.supports_color_temperature) {
 | 
			
		||||
 | 
			
		||||
  msg.disabled_by_default = light->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  for (auto mode : traits.get_supported_color_modes())
 | 
			
		||||
    msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
 | 
			
		||||
 | 
			
		||||
  msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS);
 | 
			
		||||
  msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB);
 | 
			
		||||
  msg.legacy_supports_white_value =
 | 
			
		||||
      msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) ||
 | 
			
		||||
                                  traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE));
 | 
			
		||||
  msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
 | 
			
		||||
                                          traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE);
 | 
			
		||||
 | 
			
		||||
  if (msg.legacy_supports_color_temperature) {
 | 
			
		||||
    msg.min_mireds = traits.get_min_mireds();
 | 
			
		||||
    msg.max_mireds = traits.get_max_mireds();
 | 
			
		||||
  }
 | 
			
		||||
@@ -352,6 +371,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
 | 
			
		||||
    call.set_state(msg.state);
 | 
			
		||||
  if (msg.has_brightness)
 | 
			
		||||
    call.set_brightness(msg.brightness);
 | 
			
		||||
  if (msg.has_color_mode)
 | 
			
		||||
    call.set_color_mode(static_cast<light::ColorMode>(msg.color_mode));
 | 
			
		||||
  if (msg.has_color_brightness)
 | 
			
		||||
    call.set_color_brightness(msg.color_brightness);
 | 
			
		||||
  if (msg.has_rgb) {
 | 
			
		||||
    call.set_red(msg.red);
 | 
			
		||||
    call.set_green(msg.green);
 | 
			
		||||
@@ -361,6 +384,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
 | 
			
		||||
    call.set_white(msg.white);
 | 
			
		||||
  if (msg.has_color_temperature)
 | 
			
		||||
    call.set_color_temperature(msg.color_temperature);
 | 
			
		||||
  if (msg.has_cold_white)
 | 
			
		||||
    call.set_cold_white(msg.cold_white);
 | 
			
		||||
  if (msg.has_warm_white)
 | 
			
		||||
    call.set_warm_white(msg.warm_white);
 | 
			
		||||
  if (msg.has_transition_length)
 | 
			
		||||
    call.set_transition_length(msg.transition_length);
 | 
			
		||||
  if (msg.has_flash_length)
 | 
			
		||||
@@ -395,6 +422,9 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
 | 
			
		||||
  msg.accuracy_decimals = sensor->get_accuracy_decimals();
 | 
			
		||||
  msg.force_update = sensor->get_force_update();
 | 
			
		||||
  msg.device_class = sensor->get_device_class();
 | 
			
		||||
  msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
 | 
			
		||||
  msg.disabled_by_default = sensor->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  return this->send_list_entities_sensor_response(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -417,6 +447,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) {
 | 
			
		||||
  msg.unique_id = get_default_unique_id("switch", a_switch);
 | 
			
		||||
  msg.icon = a_switch->get_icon();
 | 
			
		||||
  msg.assumed_state = a_switch->assumed_state();
 | 
			
		||||
  msg.disabled_by_default = a_switch->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_switch_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
 | 
			
		||||
@@ -451,6 +482,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor)
 | 
			
		||||
  if (msg.unique_id.empty())
 | 
			
		||||
    msg.unique_id = get_default_unique_id("text_sensor", text_sensor);
 | 
			
		||||
  msg.icon = text_sensor->get_icon();
 | 
			
		||||
  msg.disabled_by_default = text_sensor->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_text_sensor_response(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -473,10 +505,16 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
 | 
			
		||||
  } else {
 | 
			
		||||
    resp.target_temperature = climate->target_temperature;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_away())
 | 
			
		||||
    resp.away = climate->away;
 | 
			
		||||
  if (traits.get_supports_fan_modes())
 | 
			
		||||
    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode);
 | 
			
		||||
  if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
 | 
			
		||||
    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value())
 | 
			
		||||
    resp.custom_fan_mode = climate->custom_fan_mode.value();
 | 
			
		||||
  if (traits.get_supports_presets() && climate->preset.has_value()) {
 | 
			
		||||
    resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
 | 
			
		||||
    resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY;
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value())
 | 
			
		||||
    resp.custom_preset = climate->custom_preset.value();
 | 
			
		||||
  if (traits.get_supports_swing_modes())
 | 
			
		||||
    resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
 | 
			
		||||
  return this->send_climate_state_response(resp);
 | 
			
		||||
@@ -488,29 +526,31 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
 | 
			
		||||
  msg.object_id = climate->get_object_id();
 | 
			
		||||
  msg.name = climate->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("climate", climate);
 | 
			
		||||
 | 
			
		||||
  msg.disabled_by_default = climate->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  msg.supports_current_temperature = traits.get_supports_current_temperature();
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
 | 
			
		||||
  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
 | 
			
		||||
                    climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) {
 | 
			
		||||
    if (traits.supports_mode(mode))
 | 
			
		||||
      msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (auto mode : traits.get_supported_modes())
 | 
			
		||||
    msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
 | 
			
		||||
 | 
			
		||||
  msg.visual_min_temperature = traits.get_visual_min_temperature();
 | 
			
		||||
  msg.visual_max_temperature = traits.get_visual_max_temperature();
 | 
			
		||||
  msg.visual_temperature_step = traits.get_visual_temperature_step();
 | 
			
		||||
  msg.supports_away = traits.get_supports_away();
 | 
			
		||||
  msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
 | 
			
		||||
  msg.supports_action = traits.get_supports_action();
 | 
			
		||||
  for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO,
 | 
			
		||||
                        climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
 | 
			
		||||
                        climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) {
 | 
			
		||||
    if (traits.supports_fan_mode(fan_mode))
 | 
			
		||||
      msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
 | 
			
		||||
  }
 | 
			
		||||
  for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
 | 
			
		||||
                          climate::CLIMATE_SWING_HORIZONTAL}) {
 | 
			
		||||
    if (traits.supports_swing_mode(swing_mode))
 | 
			
		||||
      msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (auto fan_mode : traits.get_supported_fan_modes())
 | 
			
		||||
    msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
 | 
			
		||||
  for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
 | 
			
		||||
    msg.supported_custom_fan_modes.push_back(custom_fan_mode);
 | 
			
		||||
  for (auto preset : traits.get_supported_presets())
 | 
			
		||||
    msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset));
 | 
			
		||||
  for (auto const &custom_preset : traits.get_supported_custom_presets())
 | 
			
		||||
    msg.supported_custom_presets.push_back(custom_preset);
 | 
			
		||||
  for (auto swing_mode : traits.get_supported_swing_modes())
 | 
			
		||||
    msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
 | 
			
		||||
  return this->send_list_entities_climate_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
 | 
			
		||||
@@ -527,16 +567,95 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
 | 
			
		||||
    call.set_target_temperature_low(msg.target_temperature_low);
 | 
			
		||||
  if (msg.has_target_temperature_high)
 | 
			
		||||
    call.set_target_temperature_high(msg.target_temperature_high);
 | 
			
		||||
  if (msg.has_away)
 | 
			
		||||
    call.set_away(msg.away);
 | 
			
		||||
  if (msg.has_legacy_away)
 | 
			
		||||
    call.set_preset(msg.legacy_away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME);
 | 
			
		||||
  if (msg.has_fan_mode)
 | 
			
		||||
    call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
 | 
			
		||||
  if (msg.has_custom_fan_mode)
 | 
			
		||||
    call.set_fan_mode(msg.custom_fan_mode);
 | 
			
		||||
  if (msg.has_preset)
 | 
			
		||||
    call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
 | 
			
		||||
  if (msg.has_custom_preset)
 | 
			
		||||
    call.set_preset(msg.custom_preset);
 | 
			
		||||
  if (msg.has_swing_mode)
 | 
			
		||||
    call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
bool APIConnection::send_number_state(number::Number *number, float state) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  NumberStateResponse resp{};
 | 
			
		||||
  resp.key = number->get_object_id_hash();
 | 
			
		||||
  resp.state = state;
 | 
			
		||||
  resp.missing_state = !number->has_state();
 | 
			
		||||
  return this->send_number_state_response(resp);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_number_info(number::Number *number) {
 | 
			
		||||
  ListEntitiesNumberResponse msg;
 | 
			
		||||
  msg.key = number->get_object_id_hash();
 | 
			
		||||
  msg.object_id = number->get_object_id();
 | 
			
		||||
  msg.name = number->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("number", number);
 | 
			
		||||
  msg.icon = number->traits.get_icon();
 | 
			
		||||
  msg.disabled_by_default = number->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  msg.min_value = number->traits.get_min_value();
 | 
			
		||||
  msg.max_value = number->traits.get_max_value();
 | 
			
		||||
  msg.step = number->traits.get_step();
 | 
			
		||||
 | 
			
		||||
  return this->send_list_entities_number_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::number_command(const NumberCommandRequest &msg) {
 | 
			
		||||
  number::Number *number = App.get_number_by_key(msg.key);
 | 
			
		||||
  if (number == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = number->make_call();
 | 
			
		||||
  call.set_value(msg.state);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
bool APIConnection::send_select_state(select::Select *select, std::string state) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  SelectStateResponse resp{};
 | 
			
		||||
  resp.key = select->get_object_id_hash();
 | 
			
		||||
  resp.state = std::move(state);
 | 
			
		||||
  resp.missing_state = !select->has_state();
 | 
			
		||||
  return this->send_select_state_response(resp);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_select_info(select::Select *select) {
 | 
			
		||||
  ListEntitiesSelectResponse msg;
 | 
			
		||||
  msg.key = select->get_object_id_hash();
 | 
			
		||||
  msg.object_id = select->get_object_id();
 | 
			
		||||
  msg.name = select->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("select", select);
 | 
			
		||||
  msg.icon = select->traits.get_icon();
 | 
			
		||||
  msg.disabled_by_default = select->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  for (const auto &option : select->traits.get_options())
 | 
			
		||||
    msg.options.push_back(option);
 | 
			
		||||
 | 
			
		||||
  return this->send_list_entities_select_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
 | 
			
		||||
  select::Select *select = App.get_select_by_key(msg.key);
 | 
			
		||||
  if (select == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = select->make_call();
 | 
			
		||||
  call.set_option(msg.state);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
@@ -551,6 +670,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
 | 
			
		||||
  msg.object_id = camera->get_object_id();
 | 
			
		||||
  msg.name = camera->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("camera", camera);
 | 
			
		||||
  msg.disabled_by_default = camera->is_disabled_by_default();
 | 
			
		||||
  return this->send_list_entities_camera_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::camera_image(const CameraImageRequest &msg) {
 | 
			
		||||
@@ -579,30 +699,20 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
 | 
			
		||||
  auto buffer = this->create_buffer();
 | 
			
		||||
  // LogLevel level = 1;
 | 
			
		||||
  buffer.encode_uint32(1, static_cast<uint32_t>(level));
 | 
			
		||||
  // string tag = 2;
 | 
			
		||||
  // buffer.encode_string(2, tag, strlen(tag));
 | 
			
		||||
  // string message = 3;
 | 
			
		||||
  buffer.encode_string(3, line, strlen(line));
 | 
			
		||||
  // SubscribeLogsResponse - 29
 | 
			
		||||
  bool success = this->send_buffer(buffer, 29);
 | 
			
		||||
  if (!success) {
 | 
			
		||||
    buffer = this->create_buffer();
 | 
			
		||||
    // bool send_failed = 4;
 | 
			
		||||
    buffer.encode_bool(4, true);
 | 
			
		||||
    return this->send_buffer(buffer, 29);
 | 
			
		||||
  } else {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return this->send_buffer(buffer, 29);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
 | 
			
		||||
  this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
 | 
			
		||||
  this->client_info_ += ")";
 | 
			
		||||
  this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")";
 | 
			
		||||
  this->helper_->set_log_info(client_info_);
 | 
			
		||||
  ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
 | 
			
		||||
 | 
			
		||||
  HelloResponse resp;
 | 
			
		||||
  resp.api_version_major = 1;
 | 
			
		||||
  resp.api_version_minor = 4;
 | 
			
		||||
  resp.api_version_minor = 6;
 | 
			
		||||
  resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
 | 
			
		||||
  this->connection_state_ = ConnectionState::CONNECTED;
 | 
			
		||||
  return resp;
 | 
			
		||||
@@ -614,7 +724,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 | 
			
		||||
  // bool invalid_password = 1;
 | 
			
		||||
  resp.invalid_password = !correct;
 | 
			
		||||
  if (correct) {
 | 
			
		||||
    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
 | 
			
		||||
    ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str());
 | 
			
		||||
    this->connection_state_ = ConnectionState::AUTHENTICATED;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
@@ -632,18 +742,21 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 | 
			
		||||
  resp.mac_address = get_mac_address_pretty();
 | 
			
		||||
  resp.esphome_version = ESPHOME_VERSION;
 | 
			
		||||
  resp.compilation_time = App.get_compilation_time();
 | 
			
		||||
#ifdef ARDUINO_BOARD
 | 
			
		||||
  resp.model = ARDUINO_BOARD;
 | 
			
		||||
#endif
 | 
			
		||||
  resp.model = ESPHOME_BOARD;
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
  resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ESPHOME_PROJECT_NAME
 | 
			
		||||
  resp.project_name = ESPHOME_PROJECT_NAME;
 | 
			
		||||
  resp.project_version = ESPHOME_PROJECT_VERSION;
 | 
			
		||||
#endif
 | 
			
		||||
  return resp;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
 | 
			
		||||
  for (auto &it : this->parent_->get_state_subs())
 | 
			
		||||
    if (it.entity_id == msg.entity_id)
 | 
			
		||||
    if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
 | 
			
		||||
      it.callback(msg.state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  bool found = false;
 | 
			
		||||
@@ -657,29 +770,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
 | 
			
		||||
  for (auto &it : this->parent_->get_state_subs()) {
 | 
			
		||||
    SubscribeHomeAssistantStateResponse resp;
 | 
			
		||||
    resp.entity_id = it.entity_id;
 | 
			
		||||
    if (!this->send_subscribe_home_assistant_state_response(resp)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  state_subs_at_ = 0;
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(buffer.get_buffer()->size()).encode(header);
 | 
			
		||||
  ProtoVarInt(message_type).encode(header);
 | 
			
		||||
 | 
			
		||||
  size_t needed_space = buffer.get_buffer()->size() + header.size();
 | 
			
		||||
 | 
			
		||||
  if (needed_space > this->client_->space()) {
 | 
			
		||||
  if (!this->helper_->can_write_without_blocking()) {
 | 
			
		||||
    delay(0);
 | 
			
		||||
    if (needed_space > this->client_->space()) {
 | 
			
		||||
    APIError err = helper_->loop();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (!this->helper_->can_write_without_blocking()) {
 | 
			
		||||
      // SubscribeLogsResponse
 | 
			
		||||
      if (message_type != 29) {
 | 
			
		||||
        ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
 | 
			
		||||
@@ -689,24 +793,31 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
 | 
			
		||||
                     ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
 | 
			
		||||
  this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
 | 
			
		||||
                     ASYNC_WRITE_FLAG_COPY);
 | 
			
		||||
  bool ret = this->client_->send();
 | 
			
		||||
  return ret;
 | 
			
		||||
  APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  this->last_traffic_ = millis();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_no_setup_connection() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_fatal_error() {
 | 
			
		||||
  ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->helper_->close();
 | 
			
		||||
  this->remove_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,16 +5,17 @@
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "api_frame_helper.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
class APIConnection : public APIServerConnection {
 | 
			
		||||
 public:
 | 
			
		||||
  APIConnection(AsyncClient *client, APIServer *parent);
 | 
			
		||||
  virtual ~APIConnection();
 | 
			
		||||
  APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
 | 
			
		||||
  virtual ~APIConnection() = default;
 | 
			
		||||
 | 
			
		||||
  void disconnect_client();
 | 
			
		||||
  void start();
 | 
			
		||||
  void loop();
 | 
			
		||||
 | 
			
		||||
  bool send_list_info_done() {
 | 
			
		||||
@@ -62,6 +63,16 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool send_climate_state(climate::Climate *climate);
 | 
			
		||||
  bool send_climate_info(climate::Climate *climate);
 | 
			
		||||
  void climate_command(const ClimateCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool send_number_state(number::Number *number, float state);
 | 
			
		||||
  bool send_number_info(number::Number *number);
 | 
			
		||||
  void number_command(const NumberCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool send_select_state(select::Select *select, std::string state);
 | 
			
		||||
  bool send_select_info(select::Select *select);
 | 
			
		||||
  void select_command(const SelectCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
@@ -76,10 +87,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override {
 | 
			
		||||
    // we initiated disconnect_client
 | 
			
		||||
    this->next_close_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override;
 | 
			
		||||
  void on_ping_response(const PingResponse &value) override {
 | 
			
		||||
    // we initiated ping
 | 
			
		||||
    this->sent_ping_ = false;
 | 
			
		||||
@@ -90,12 +98,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
#endif
 | 
			
		||||
  HelloResponse hello(const HelloRequest &msg) override;
 | 
			
		||||
  ConnectResponse connect(const ConnectRequest &msg) override;
 | 
			
		||||
  DisconnectResponse disconnect(const DisconnectRequest &msg) override {
 | 
			
		||||
    // remote initiated disconnect_client
 | 
			
		||||
    this->next_close_ = true;
 | 
			
		||||
    DisconnectResponse resp;
 | 
			
		||||
    return resp;
 | 
			
		||||
  }
 | 
			
		||||
  DisconnectResponse disconnect(const DisconnectRequest &msg) override;
 | 
			
		||||
  PingResponse ping(const PingRequest &msg) override { return {}; }
 | 
			
		||||
  DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
 | 
			
		||||
  void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
 | 
			
		||||
@@ -125,19 +128,16 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void on_unauthenticated_access() override;
 | 
			
		||||
  void on_no_setup_connection() override;
 | 
			
		||||
  ProtoWriteBuffer create_buffer() override {
 | 
			
		||||
    this->send_buffer_.clear();
 | 
			
		||||
    return {&this->send_buffer_};
 | 
			
		||||
    // FIXME: ensure no recursive writes can happen
 | 
			
		||||
    this->proto_write_buffer_.clear();
 | 
			
		||||
    return {&this->proto_write_buffer_};
 | 
			
		||||
  }
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  friend APIServer;
 | 
			
		||||
 | 
			
		||||
  void on_error_(int8_t error);
 | 
			
		||||
  void on_disconnect_();
 | 
			
		||||
  void on_timeout_(uint32_t time);
 | 
			
		||||
  void on_data_(uint8_t *buf, size_t len);
 | 
			
		||||
  void parse_recv_buffer_();
 | 
			
		||||
  bool send_(const void *buf, size_t len, bool force);
 | 
			
		||||
 | 
			
		||||
  enum class ConnectionState {
 | 
			
		||||
    WAITING_FOR_HELLO,
 | 
			
		||||
@@ -147,8 +147,10 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
 | 
			
		||||
  bool remove_{false};
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> send_buffer_;
 | 
			
		||||
  std::vector<uint8_t> recv_buffer_;
 | 
			
		||||
  // Buffer used to encode proto messages
 | 
			
		||||
  // Re-use to prevent allocations
 | 
			
		||||
  std::vector<uint8_t> proto_write_buffer_;
 | 
			
		||||
  std::unique_ptr<APIFrameHelper> helper_;
 | 
			
		||||
 | 
			
		||||
  std::string client_info_;
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
@@ -160,12 +162,11 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  uint32_t last_traffic_;
 | 
			
		||||
  bool sent_ping_{false};
 | 
			
		||||
  bool service_call_subscription_{false};
 | 
			
		||||
  bool current_nodelay_{false};
 | 
			
		||||
  bool next_close_{false};
 | 
			
		||||
  AsyncClient *client_;
 | 
			
		||||
  bool next_close_ = false;
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
  int state_subs_at_ = -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										982
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										982
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,982 @@
 | 
			
		||||
#include "api_frame_helper.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.socket";
 | 
			
		||||
 | 
			
		||||
/// Is the given return value (from read/write syscalls) a wouldblock error?
 | 
			
		||||
bool is_would_block(ssize_t ret) {
 | 
			
		||||
  if (ret == -1) {
 | 
			
		||||
    return errno == EWOULDBLOCK || errno == EAGAIN;
 | 
			
		||||
  }
 | 
			
		||||
  return ret == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *api_error_to_str(APIError err) {
 | 
			
		||||
  // not using switch to ensure compiler doesn't try to build a big table out of it
 | 
			
		||||
  if (err == APIError::OK) {
 | 
			
		||||
    return "OK";
 | 
			
		||||
  } else if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    return "WOULD_BLOCK";
 | 
			
		||||
  } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
    return "BAD_HANDSHAKE_PACKET_LEN";
 | 
			
		||||
  } else if (err == APIError::BAD_INDICATOR) {
 | 
			
		||||
    return "BAD_INDICATOR";
 | 
			
		||||
  } else if (err == APIError::BAD_DATA_PACKET) {
 | 
			
		||||
    return "BAD_DATA_PACKET";
 | 
			
		||||
  } else if (err == APIError::TCP_NODELAY_FAILED) {
 | 
			
		||||
    return "TCP_NODELAY_FAILED";
 | 
			
		||||
  } else if (err == APIError::TCP_NONBLOCKING_FAILED) {
 | 
			
		||||
    return "TCP_NONBLOCKING_FAILED";
 | 
			
		||||
  } else if (err == APIError::CLOSE_FAILED) {
 | 
			
		||||
    return "CLOSE_FAILED";
 | 
			
		||||
  } else if (err == APIError::SHUTDOWN_FAILED) {
 | 
			
		||||
    return "SHUTDOWN_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_STATE) {
 | 
			
		||||
    return "BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::BAD_ARG) {
 | 
			
		||||
    return "BAD_ARG";
 | 
			
		||||
  } else if (err == APIError::SOCKET_READ_FAILED) {
 | 
			
		||||
    return "SOCKET_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::SOCKET_WRITE_FAILED) {
 | 
			
		||||
    return "SOCKET_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
 | 
			
		||||
    return "HANDSHAKESTATE_BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
 | 
			
		||||
    return "CIPHERSTATE_DECRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
 | 
			
		||||
    return "CIPHERSTATE_ENCRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::OUT_OF_MEMORY) {
 | 
			
		||||
    return "OUT_OF_MEMORY";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_SETUP_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_SPLIT_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
 | 
			
		||||
    return "BAD_HANDSHAKE_ERROR_BYTE";
 | 
			
		||||
  }
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
 | 
			
		||||
 | 
			
		||||
/// Convert a noise error code to a readable error
 | 
			
		||||
std::string noise_err_to_str(int err) {
 | 
			
		||||
  if (err == NOISE_ERROR_NO_MEMORY)
 | 
			
		||||
    return "NO_MEMORY";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_ID)
 | 
			
		||||
    return "UNKNOWN_ID";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_NAME)
 | 
			
		||||
    return "UNKNOWN_NAME";
 | 
			
		||||
  if (err == NOISE_ERROR_MAC_FAILURE)
 | 
			
		||||
    return "MAC_FAILURE";
 | 
			
		||||
  if (err == NOISE_ERROR_NOT_APPLICABLE)
 | 
			
		||||
    return "NOT_APPLICABLE";
 | 
			
		||||
  if (err == NOISE_ERROR_SYSTEM)
 | 
			
		||||
    return "SYSTEM";
 | 
			
		||||
  if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
 | 
			
		||||
    return "REMOTE_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
 | 
			
		||||
    return "LOCAL_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_PSK_REQUIRED)
 | 
			
		||||
    return "PSK_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_LENGTH)
 | 
			
		||||
    return "INVALID_LENGTH";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PARAM)
 | 
			
		||||
    return "INVALID_PARAM";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_STATE)
 | 
			
		||||
    return "INVALID_STATE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_NONCE)
 | 
			
		||||
    return "INVALID_NONCE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
 | 
			
		||||
    return "INVALID_PRIVATE_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
 | 
			
		||||
    return "INVALID_PUBLIC_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_FORMAT)
 | 
			
		||||
    return "INVALID_FORMAT";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_SIGNATURE)
 | 
			
		||||
    return "INVALID_SIGNATURE";
 | 
			
		||||
  return to_string(err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APINoiseFrameHelper::init() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // init prologue
 | 
			
		||||
  prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT));
 | 
			
		||||
 | 
			
		||||
  state_ = State::CLIENT_HELLO;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/// Run through handshake messages (if in that phase)
 | 
			
		||||
APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  APIError err = state_action_();
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (err != APIError::OK)
 | 
			
		||||
    return err;
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 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_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.
 | 
			
		||||
 *
 | 
			
		||||
 * 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_(ParsedFrame *frame) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
    size_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not a full read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // header reading done
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read body
 | 
			
		||||
  uint8_t indicator = rx_header_buf_[0];
 | 
			
		||||
  if (indicator != 0x01) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad indicator byte %u", indicator);
 | 
			
		||||
    return APIError::BAD_INDICATOR;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad packet len for handshake: %d", msg_size);
 | 
			
		||||
    return APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != msg_size) {
 | 
			
		||||
    rx_buf_.resize(msg_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    size_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  frame->msg = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_len_ = 0;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** To be called from read/write methods.
 | 
			
		||||
 *
 | 
			
		||||
 * This method runs through the internal handshake methods, if in that state.
 | 
			
		||||
 *
 | 
			
		||||
 * If the handshake is still active when this method returns and a read/write can't take place at
 | 
			
		||||
 * the moment, returns WOULD_BLOCK.
 | 
			
		||||
 * If an error occured, returns that error. Only returns OK if the transport is ready for data
 | 
			
		||||
 * traffic.
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  if (state_ == State::INITIALIZE) {
 | 
			
		||||
    HELPER_LOG("Bad state for method: %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLIENT_HELLO) {
 | 
			
		||||
    // waiting for client hello
 | 
			
		||||
    ParsedFrame frame;
 | 
			
		||||
    aerr = try_read_frame_(&frame);
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      send_explicit_handshake_reject_("Bad indicator byte");
 | 
			
		||||
      return aerr;
 | 
			
		||||
    }
 | 
			
		||||
    if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
      send_explicit_handshake_reject_("Bad handshake packet len");
 | 
			
		||||
      return aerr;
 | 
			
		||||
    }
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
    // ignore contents, may be used in future for flags
 | 
			
		||||
    prologue_.push_back((uint8_t)(frame.msg.size() >> 8));
 | 
			
		||||
    prologue_.push_back((uint8_t) frame.msg.size());
 | 
			
		||||
    prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
 | 
			
		||||
 | 
			
		||||
    state_ = State::SERVER_HELLO;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::SERVER_HELLO) {
 | 
			
		||||
    // send server hello
 | 
			
		||||
    uint8_t msg[1];
 | 
			
		||||
    msg[0] = 0x01;  // chosen proto
 | 
			
		||||
    aerr = write_frame_(msg, 1);
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
 | 
			
		||||
    // start handshake
 | 
			
		||||
    aerr = init_handshake_();
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
 | 
			
		||||
    state_ = State::HANDSHAKE;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::HANDSHAKE) {
 | 
			
		||||
    int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
    if (action == NOISE_ACTION_READ_MESSAGE) {
 | 
			
		||||
      // waiting for handshake msg
 | 
			
		||||
      ParsedFrame frame;
 | 
			
		||||
      aerr = try_read_frame_(&frame);
 | 
			
		||||
      if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
        send_explicit_handshake_reject_("Bad indicator byte");
 | 
			
		||||
        return aerr;
 | 
			
		||||
      }
 | 
			
		||||
      if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
        send_explicit_handshake_reject_("Bad handshake packet len");
 | 
			
		||||
        return aerr;
 | 
			
		||||
      }
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
 | 
			
		||||
      if (frame.msg.empty()) {
 | 
			
		||||
        send_explicit_handshake_reject_("Empty handshake message");
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      } else if (frame.msg[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
 | 
			
		||||
        send_explicit_handshake_reject_("Bad handshake error byte");
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
 | 
			
		||||
      err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
        if (err == NOISE_ERROR_MAC_FAILURE) {
 | 
			
		||||
          send_explicit_handshake_reject_("Handshake MAC failure");
 | 
			
		||||
        } else {
 | 
			
		||||
          send_explicit_handshake_reject_("Handshake error");
 | 
			
		||||
        }
 | 
			
		||||
        return APIError::HANDSHAKESTATE_READ_FAILED;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      aerr = check_handshake_finished_();
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
    } else if (action == NOISE_ACTION_WRITE_MESSAGE) {
 | 
			
		||||
      uint8_t buffer[65];
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
 | 
			
		||||
 | 
			
		||||
      err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
        return APIError::HANDSHAKESTATE_WRITE_FAILED;
 | 
			
		||||
      }
 | 
			
		||||
      buffer[0] = 0x00;  // success
 | 
			
		||||
 | 
			
		||||
      aerr = write_frame_(buffer, mbuf.size + 1);
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
      aerr = check_handshake_finished_();
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
    } else {
 | 
			
		||||
      // bad state for action
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad action for handshake: %d", action);
 | 
			
		||||
      return APIError::HANDSHAKESTATE_BAD_STATE;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLOSED || state_ == State::FAILED) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
 | 
			
		||||
  std::vector<uint8_t> data;
 | 
			
		||||
  data.resize(reason.length() + 1);
 | 
			
		||||
  data[0] = 0x01;  // failure
 | 
			
		||||
  for (size_t i = 0; i < reason.length(); i++) {
 | 
			
		||||
    data[i + 1] = (uint8_t) reason[i];
 | 
			
		||||
  }
 | 
			
		||||
  // temporarily remove failed state
 | 
			
		||||
  auto orig_state = state_;
 | 
			
		||||
  state_ = State::EXPLICIT_REJECT;
 | 
			
		||||
  write_frame_(data.data(), data.size());
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
 | 
			
		||||
  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::CIPHERSTATE_DECRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.msg.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: size %d too short", msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uint16_t type;
 | 
			
		||||
  // uint16_t data_len;
 | 
			
		||||
  // uint8_t *data;
 | 
			
		||||
  // uint8_t *padding;  zero or more bytes to fill up the rest of the packet
 | 
			
		||||
  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;
 | 
			
		||||
    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.msg);
 | 
			
		||||
  buffer->data_offset = 4;
 | 
			
		||||
  buffer->data_len = data_len;
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t padding = 0;
 | 
			
		||||
  size_t msg_len = 4 + payload_len + padding;
 | 
			
		||||
  size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
 | 
			
		||||
  auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
 | 
			
		||||
  if (tmpbuf == nullptr) {
 | 
			
		||||
    HELPER_LOG("Could not allocate for writing packet");
 | 
			
		||||
    return APIError::OUT_OF_MEMORY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  tmpbuf[0] = 0x01;  // indicator
 | 
			
		||||
  // tmpbuf[1], tmpbuf[2] to be set later
 | 
			
		||||
  const uint8_t msg_offset = 3;
 | 
			
		||||
  const uint8_t payload_offset = msg_offset + 4;
 | 
			
		||||
  tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8);  // type
 | 
			
		||||
  tmpbuf[msg_offset + 1] = (uint8_t) type;
 | 
			
		||||
  tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8);  // data_len
 | 
			
		||||
  tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
 | 
			
		||||
  // copy data
 | 
			
		||||
  std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
 | 
			
		||||
  // fill padding with zeros
 | 
			
		||||
  std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
 | 
			
		||||
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::CIPHERSTATE_ENCRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t total_len = 3 + mbuf.size;
 | 
			
		||||
  tmpbuf[1] = (uint8_t)(mbuf.size >> 8);
 | 
			
		||||
  tmpbuf[2] = (uint8_t) mbuf.size;
 | 
			
		||||
 | 
			
		||||
  struct iovec iov;
 | 
			
		||||
  iov.iov_base = &tmpbuf[0];
 | 
			
		||||
  iov.iov_len = total_len;
 | 
			
		||||
 | 
			
		||||
  // write raw to not have two packets sent if NAGLE disabled
 | 
			
		||||
  return write_raw_(&iov, 1);
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN)
 | 
			
		||||
        break;
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    total_write_len += iov[i].iov_len;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occured
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if (sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t to_consume = sent;
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        to_consume -= iov[i].iov_len;
 | 
			
		||||
      } else {
 | 
			
		||||
        tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
 | 
			
		||||
                       reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
 | 
			
		||||
  uint8_t header[3];
 | 
			
		||||
  header[0] = 0x01;  // indicator
 | 
			
		||||
  header[1] = (uint8_t)(len >> 8);
 | 
			
		||||
  header[2] = (uint8_t) len;
 | 
			
		||||
 | 
			
		||||
  struct iovec iov[2];
 | 
			
		||||
  iov[0].iov_base = header;
 | 
			
		||||
  iov[0].iov_len = 3;
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(data);
 | 
			
		||||
  iov[1].iov_len = len;
 | 
			
		||||
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Initiate the data structures for the handshake.
 | 
			
		||||
 *
 | 
			
		||||
 * @return 0 on success, -1 on error (check errno)
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::init_handshake_() {
 | 
			
		||||
  int err;
 | 
			
		||||
  memset(&nid_, 0, sizeof(nid_));
 | 
			
		||||
  // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
 | 
			
		||||
  // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
 | 
			
		||||
  nid_.pattern_id = NOISE_PATTERN_NN;
 | 
			
		||||
  nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
 | 
			
		||||
  nid_.dh_id = NOISE_DH_CURVE25519;
 | 
			
		||||
  nid_.prefix_id = NOISE_PREFIX_STANDARD;
 | 
			
		||||
  nid_.hybrid_id = NOISE_DH_NONE;
 | 
			
		||||
  nid_.hash_id = NOISE_HASH_SHA256;
 | 
			
		||||
  nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const auto &psk = ctx_->get_psk();
 | 
			
		||||
  err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  // set_prologue copies it into handshakestate, so we can get rid of it now
 | 
			
		||||
  prologue_ = {};
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_start(handshake_);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::check_handshake_finished_() {
 | 
			
		||||
  assert(state_ == State::HANDSHAKE);
 | 
			
		||||
 | 
			
		||||
  int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
  if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (action != NOISE_ACTION_SPLIT) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad action for handshake: %d", action);
 | 
			
		||||
    return APIError::HANDSHAKESTATE_BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SPLIT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  HELPER_LOG("Handshake complete!");
 | 
			
		||||
  noise_handshakestate_free(handshake_);
 | 
			
		||||
  handshake_ = nullptr;
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APINoiseFrameHelper::~APINoiseFrameHelper() {
 | 
			
		||||
  if (handshake_ != nullptr) {
 | 
			
		||||
    noise_handshakestate_free(handshake_);
 | 
			
		||||
    handshake_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  if (send_cipher_ != nullptr) {
 | 
			
		||||
    noise_cipherstate_free(send_cipher_);
 | 
			
		||||
    send_cipher_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  if (recv_cipher_ != nullptr) {
 | 
			
		||||
    noise_cipherstate_free(recv_cipher_);
 | 
			
		||||
    recv_cipher_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
extern "C" {
 | 
			
		||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
 | 
			
		||||
void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APIPlaintextFrameHelper::init() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/// Not used for plaintext
 | 
			
		||||
APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  // try send pending TX data
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 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
 | 
			
		||||
 *
 | 
			
		||||
 * @return See APIError
 | 
			
		||||
 *
 | 
			
		||||
 * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    uint8_t data;
 | 
			
		||||
    ssize_t received = socket_->read(&data, 1);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_.push_back(data);
 | 
			
		||||
 | 
			
		||||
    // try parse header
 | 
			
		||||
    if (rx_header_buf_[0] != 0x00) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
 | 
			
		||||
      return APIError::BAD_INDICATOR;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t i = 1;
 | 
			
		||||
    uint32_t consumed = 0;
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    if (!msg_size_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint32();
 | 
			
		||||
    rx_header_parsed_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != rx_header_parsed_len_) {
 | 
			
		||||
    rx_buf_.resize(rx_header_parsed_len_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  frame->msg = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_.clear();
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame.msg);
 | 
			
		||||
  buffer->data_offset = 0;
 | 
			
		||||
  buffer->data_len = rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(payload_len).encode(header);
 | 
			
		||||
  ProtoVarInt(type).encode(header);
 | 
			
		||||
 | 
			
		||||
  struct iovec iov[2];
 | 
			
		||||
  iov[0].iov_base = &header[0];
 | 
			
		||||
  iov[0].iov_len = header.size();
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(payload);
 | 
			
		||||
  iov[1].iov_len = payload_len;
 | 
			
		||||
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (is_would_block(sent)) {
 | 
			
		||||
      break;
 | 
			
		||||
    } else if (sent == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    total_write_len += iov[i].iov_len;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occured
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if (sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t to_consume = sent;
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        to_consume -= iov[i].iov_len;
 | 
			
		||||
      } else {
 | 
			
		||||
        tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
 | 
			
		||||
                       reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										183
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,183 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <deque>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
#include "noise/protocol.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
struct ReadPacketBuffer {
 | 
			
		||||
  std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  size_t data_offset;
 | 
			
		||||
  size_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct PacketBuffer {
 | 
			
		||||
  const std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint8_t data_offset;
 | 
			
		||||
  uint8_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class APIError : int {
 | 
			
		||||
  OK = 0,
 | 
			
		||||
  WOULD_BLOCK = 1001,
 | 
			
		||||
  BAD_HANDSHAKE_PACKET_LEN = 1002,
 | 
			
		||||
  BAD_INDICATOR = 1003,
 | 
			
		||||
  BAD_DATA_PACKET = 1004,
 | 
			
		||||
  TCP_NODELAY_FAILED = 1005,
 | 
			
		||||
  TCP_NONBLOCKING_FAILED = 1006,
 | 
			
		||||
  CLOSE_FAILED = 1007,
 | 
			
		||||
  SHUTDOWN_FAILED = 1008,
 | 
			
		||||
  BAD_STATE = 1009,
 | 
			
		||||
  BAD_ARG = 1010,
 | 
			
		||||
  SOCKET_READ_FAILED = 1011,
 | 
			
		||||
  SOCKET_WRITE_FAILED = 1012,
 | 
			
		||||
  HANDSHAKESTATE_READ_FAILED = 1013,
 | 
			
		||||
  HANDSHAKESTATE_WRITE_FAILED = 1014,
 | 
			
		||||
  HANDSHAKESTATE_BAD_STATE = 1015,
 | 
			
		||||
  CIPHERSTATE_DECRYPT_FAILED = 1016,
 | 
			
		||||
  CIPHERSTATE_ENCRYPT_FAILED = 1017,
 | 
			
		||||
  OUT_OF_MEMORY = 1018,
 | 
			
		||||
  HANDSHAKESTATE_SETUP_FAILED = 1019,
 | 
			
		||||
  HANDSHAKESTATE_SPLIT_FAILED = 1020,
 | 
			
		||||
  BAD_HANDSHAKE_ERROR_BYTE = 1021,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const char *api_error_to_str(APIError err);
 | 
			
		||||
 | 
			
		||||
class APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual ~APIFrameHelper() = default;
 | 
			
		||||
  virtual APIError init() = 0;
 | 
			
		||||
  virtual APIError loop() = 0;
 | 
			
		||||
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
 | 
			
		||||
  virtual bool can_write_without_blocking() = 0;
 | 
			
		||||
  virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual std::string getpeername() = 0;
 | 
			
		||||
  virtual APIError close() = 0;
 | 
			
		||||
  virtual APIError shutdown(int how) = 0;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  virtual void set_log_info(std::string info) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
 | 
			
		||||
      : socket_(std::move(socket)), ctx_(ctx) {}
 | 
			
		||||
  ~APINoiseFrameHelper();
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  std::string getpeername() override { return socket_->getpeername(); }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, size_t len);
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  uint8_t rx_header_buf_[3];
 | 
			
		||||
  size_t rx_header_buf_len_ = 0;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
  std::vector<uint8_t> prologue_;
 | 
			
		||||
 | 
			
		||||
  std::shared_ptr<APINoiseContext> ctx_;
 | 
			
		||||
  NoiseHandshakeState *handshake_ = nullptr;
 | 
			
		||||
  NoiseCipherState *send_cipher_ = nullptr;
 | 
			
		||||
  NoiseCipherState *recv_cipher_ = nullptr;
 | 
			
		||||
  NoiseProtocolId nid_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    CLIENT_HELLO = 2,
 | 
			
		||||
    SERVER_HELLO = 3,
 | 
			
		||||
    HANDSHAKE = 4,
 | 
			
		||||
    DATA = 5,
 | 
			
		||||
    CLOSED = 6,
 | 
			
		||||
    FAILED = 7,
 | 
			
		||||
    EXPLICIT_REJECT = 8,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
 | 
			
		||||
  ~APIPlaintextFrameHelper() = default;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  std::string getpeername() override { return socket_->getpeername(); }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  std::vector<uint8_t> rx_header_buf_;
 | 
			
		||||
  bool rx_header_parsed_ = false;
 | 
			
		||||
  uint32_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint32_t rx_header_parsed_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    DATA = 2,
 | 
			
		||||
    CLOSED = 3,
 | 
			
		||||
    FAILED = 4,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
using psk_t = std::array<uint8_t, 32>;
 | 
			
		||||
 | 
			
		||||
class APINoiseContext {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_psk(psk_t psk) { psk_ = std::move(psk); }
 | 
			
		||||
  const psk_t &get_psk() const { return psk_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  psk_t psk_;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,61 +6,85 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "api.service";
 | 
			
		||||
static const char *const TAG = "api.service";
 | 
			
		||||
 | 
			
		||||
bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<HelloResponse>(msg, 2);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ConnectResponse>(msg, 4);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<DisconnectRequest>(msg, 5);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<DisconnectResponse>(msg, 6);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<PingRequest>(msg, 7);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<PingResponse>(msg, 8);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<DeviceInfoResponse>(msg, 10);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesDoneResponse>(msg, 19);
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesBinarySensorResponse>(msg, 12);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<BinarySensorStateResponse>(msg, 21);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesCoverResponse>(msg, 13);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<CoverStateResponse>(msg, 22);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -68,13 +92,17 @@ bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesFanResponse>(msg, 14);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<FanStateResponse>(msg, 23);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -82,13 +110,17 @@ bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &ms
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesLightResponse>(msg, 15);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<LightStateResponse>(msg, 24);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -96,25 +128,33 @@ bool APIServerConnectionBase::send_light_state_response(const LightStateResponse
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesSensorResponse>(msg, 16);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<SensorStateResponse>(msg, 25);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesSwitchResponse>(msg, 17);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<SwitchStateResponse>(msg, 26);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -122,13 +162,17 @@ bool APIServerConnectionBase::send_switch_state_response(const SwitchStateRespon
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesTextSensorResponse>(msg, 18);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<TextSensorStateResponse>(msg, 27);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -136,35 +180,49 @@ bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsRe
 | 
			
		||||
  return this->send_message_<SubscribeLogsResponse>(msg, 29);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<HomeassistantServiceResponse>(msg, 35);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_subscribe_home_assistant_state_response(
 | 
			
		||||
    const SubscribeHomeAssistantStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<SubscribeHomeAssistantStateResponse>(msg, 39);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<GetTimeRequest>(msg, 36);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<GetTimeResponse>(msg, 37);
 | 
			
		||||
}
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesServicesResponse>(msg, 41);
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesCameraResponse>(msg, 43);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<CameraImageResponse>(msg, 44);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -172,87 +230,147 @@ bool APIServerConnectionBase::send_camera_image_response(const CameraImageRespon
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesClimateResponse>(msg, 46);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ClimateStateResponse>(msg, 47);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_number_response(const ListEntitiesNumberResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_number_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesNumberResponse>(msg, 49);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
bool APIServerConnectionBase::send_number_state_response(const NumberStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_number_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<NumberStateResponse>(msg, 50);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_select_response(const ListEntitiesSelectResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_select_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesSelectResponse>(msg, 52);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
bool APIServerConnectionBase::send_select_state_response(const SelectStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_select_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<SelectStateResponse>(msg, 53);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
#endif
 | 
			
		||||
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
 | 
			
		||||
  switch (msg_type) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
      HelloRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_hello_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 3: {
 | 
			
		||||
      ConnectRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_connect_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 5: {
 | 
			
		||||
      DisconnectRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_disconnect_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 6: {
 | 
			
		||||
      DisconnectResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_disconnect_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 7: {
 | 
			
		||||
      PingRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_ping_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 8: {
 | 
			
		||||
      PingResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_ping_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 9: {
 | 
			
		||||
      DeviceInfoRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_device_info_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 11: {
 | 
			
		||||
      ListEntitiesRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_list_entities_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 20: {
 | 
			
		||||
      SubscribeStatesRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_subscribe_states_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 28: {
 | 
			
		||||
      SubscribeLogsRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_subscribe_logs_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -260,7 +378,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
      CoverCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_cover_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -269,7 +389,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
      FanCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_fan_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -278,7 +400,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
      LightCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_light_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -287,7 +411,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      SwitchCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_switch_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -295,42 +421,54 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
    case 34: {
 | 
			
		||||
      SubscribeHomeassistantServicesRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_subscribe_homeassistant_services_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 36: {
 | 
			
		||||
      GetTimeRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_get_time_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 37: {
 | 
			
		||||
      GetTimeResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_get_time_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 38: {
 | 
			
		||||
      SubscribeHomeAssistantStatesRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_subscribe_home_assistant_states_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 40: {
 | 
			
		||||
      HomeAssistantStateResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_home_assistant_state_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 42: {
 | 
			
		||||
      ExecuteServiceRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_execute_service_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -338,7 +476,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
      CameraImageRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_camera_image_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -347,8 +487,32 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
      ClimateCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_climate_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 51: {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      NumberCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_number_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 54: {
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      SelectCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_select_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -547,6 +711,32 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
 | 
			
		||||
  this->climate_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->number_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->select_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,24 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  virtual void on_climate_command_request(const ClimateCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool send_list_entities_number_response(const ListEntitiesNumberResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool send_number_state_response(const NumberStateResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  virtual void on_number_command_request(const NumberCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool send_list_entities_select_response(const ListEntitiesSelectResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool send_select_state_response(const SelectStateResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  virtual void on_select_command_request(const SelectCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
 | 
			
		||||
@@ -147,6 +165,12 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  virtual void climate_command(const ClimateCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  virtual void number_command(const NumberCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  virtual void select_command(const SelectCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  void on_hello_request(const HelloRequest &msg) override;
 | 
			
		||||
@@ -179,6 +203,12 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  void on_climate_command_request(const ClimateCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void on_number_command_request(const NumberCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_command_request(const SelectCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "api_connection.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
@@ -15,26 +16,51 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "api";
 | 
			
		||||
static const char *const TAG = "api";
 | 
			
		||||
 | 
			
		||||
// APIServer
 | 
			
		||||
void APIServer::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
 | 
			
		||||
  this->setup_controller();
 | 
			
		||||
  this->server_ = AsyncServer(this->port_);
 | 
			
		||||
  this->server_.setNoDelay(false);
 | 
			
		||||
  this->server_.begin();
 | 
			
		||||
  this->server_.onClient(
 | 
			
		||||
      [](void *s, AsyncClient *client) {
 | 
			
		||||
        if (client == nullptr)
 | 
			
		||||
          return;
 | 
			
		||||
  socket_ = socket::socket(AF_INET, SOCK_STREAM, 0);
 | 
			
		||||
  if (socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket.");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
 | 
			
		||||
    // we can still continue
 | 
			
		||||
  }
 | 
			
		||||
  err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct sockaddr_in server;
 | 
			
		||||
  memset(&server, 0, sizeof(server));
 | 
			
		||||
  server.sin_family = AF_INET;
 | 
			
		||||
  server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
 | 
			
		||||
  server.sin_port = htons(this->port_);
 | 
			
		||||
 | 
			
		||||
  err = socket_->bind((struct sockaddr *) &server, sizeof(server));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = socket_->listen(4);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
        // can't print here because in lwIP thread
 | 
			
		||||
        // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
 | 
			
		||||
        auto *a_this = (APIServer *) s;
 | 
			
		||||
        a_this->clients_.push_back(new APIConnection(client, a_this));
 | 
			
		||||
      },
 | 
			
		||||
      this);
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (logger::global_logger != nullptr) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
 | 
			
		||||
@@ -59,12 +85,26 @@ void APIServer::setup() {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void APIServer::loop() {
 | 
			
		||||
  // Accept new clients
 | 
			
		||||
  while (true) {
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
    auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (!sock)
 | 
			
		||||
      break;
 | 
			
		||||
    ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
 | 
			
		||||
 | 
			
		||||
    auto *conn = new APIConnection(std::move(sock), this);
 | 
			
		||||
    clients_.push_back(conn);
 | 
			
		||||
    conn->start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Partition clients into remove and active
 | 
			
		||||
  auto new_end =
 | 
			
		||||
      std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
 | 
			
		||||
  // print disconnection messages
 | 
			
		||||
  for (auto it = new_end; it != this->clients_.end(); ++it) {
 | 
			
		||||
    ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
 | 
			
		||||
    ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  // only then delete the pointers, otherwise log routine
 | 
			
		||||
  // would access freed memory
 | 
			
		||||
@@ -180,7 +220,7 @@ void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) {
 | 
			
		||||
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
@@ -197,9 +237,27 @@ void APIServer::on_climate_update(climate::Climate *obj) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
    c->send_number_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
    c->send_select_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
 | 
			
		||||
APIServer *global_api_server = nullptr;
 | 
			
		||||
APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
 | 
			
		||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
@@ -208,9 +266,11 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
APIServer::APIServer() { global_api_server = this; }
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f) {
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                               std::function<void(std::string)> f) {
 | 
			
		||||
  this->state_subs_.push_back(HomeAssistantStateSubscription{
 | 
			
		||||
      .entity_id = std::move(entity_id),
 | 
			
		||||
      .attribute = std::move(attribute),
 | 
			
		||||
      .callback = std::move(f),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,20 +4,14 @@
 | 
			
		||||
#include "esphome/core/controller.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
#include "list_entities.h"
 | 
			
		||||
#include "subscribe_state.h"
 | 
			
		||||
#include "homeassistant_service.h"
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#include <AsyncTCP.h>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#include <ESPAsyncTCP.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -36,6 +30,12 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void set_port(uint16_t port);
 | 
			
		||||
  void set_password(const std::string &password);
 | 
			
		||||
  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); }
 | 
			
		||||
  std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
  void handle_disconnect(APIConnection *conn);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
 | 
			
		||||
@@ -56,10 +56,16 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void on_switch_update(switch_::Switch *obj, bool state) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override;
 | 
			
		||||
  void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  void on_climate_update(climate::Climate *obj) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void on_number_update(number::Number *obj, float state) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_update(select::Select *obj, const std::string &state) override;
 | 
			
		||||
#endif
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
@@ -71,15 +77,17 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
 | 
			
		||||
  struct HomeAssistantStateSubscription {
 | 
			
		||||
    std::string entity_id;
 | 
			
		||||
    optional<std::string> attribute;
 | 
			
		||||
    std::function<void(std::string)> callback;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  void subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f);
 | 
			
		||||
  void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                      std::function<void(std::string)> f);
 | 
			
		||||
  const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
 | 
			
		||||
  const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AsyncServer server_{0};
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_ = nullptr;
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
  uint32_t reboot_timeout_{300000};
 | 
			
		||||
  uint32_t last_connected_{0};
 | 
			
		||||
@@ -87,9 +95,13 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  std::string password_;
 | 
			
		||||
  std::vector<HomeAssistantStateSubscription> state_subs_;
 | 
			
		||||
  std::vector<UserServiceDescriptor *> user_services_;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern APIServer *global_api_server;
 | 
			
		||||
extern APIServer *global_api_server;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
 | 
			
		||||
import zeroconf
 | 
			
		||||
 | 
			
		||||
from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
 | 
			
		||||
from esphome.util import safe_print
 | 
			
		||||
from . import CONF_ENCRYPTION
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def async_run_logs(config, address):
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    port: int = int(conf[CONF_PORT])
 | 
			
		||||
    password: str = conf[CONF_PASSWORD]
 | 
			
		||||
    noise_psk: Optional[str] = None
 | 
			
		||||
    if CONF_ENCRYPTION in conf:
 | 
			
		||||
        noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
    cli = APIClient(
 | 
			
		||||
        asyncio.get_event_loop(),
 | 
			
		||||
        address,
 | 
			
		||||
        port,
 | 
			
		||||
        password,
 | 
			
		||||
        client_info=f"ESPHome Logs {__version__}",
 | 
			
		||||
        noise_psk=noise_psk,
 | 
			
		||||
    )
 | 
			
		||||
    first_connect = True
 | 
			
		||||
 | 
			
		||||
    def on_log(msg):
 | 
			
		||||
        time_ = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
        text = msg.message.decode("utf8", "backslashreplace")
 | 
			
		||||
        safe_print(time_ + text)
 | 
			
		||||
 | 
			
		||||
    async def on_connect():
 | 
			
		||||
        nonlocal first_connect
 | 
			
		||||
        try:
 | 
			
		||||
            await cli.subscribe_logs(
 | 
			
		||||
                on_log,
 | 
			
		||||
                log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
 | 
			
		||||
                dump_config=first_connect,
 | 
			
		||||
            )
 | 
			
		||||
            first_connect = False
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            cli.disconnect()
 | 
			
		||||
 | 
			
		||||
    async def on_disconnect():
 | 
			
		||||
        _LOGGER.warning("Disconnected from API")
 | 
			
		||||
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
    reconnect = ReconnectLogic(
 | 
			
		||||
        client=cli,
 | 
			
		||||
        on_connect=on_connect,
 | 
			
		||||
        on_disconnect=on_disconnect,
 | 
			
		||||
        zeroconf_instance=zc,
 | 
			
		||||
    )
 | 
			
		||||
    await reconnect.start()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        while True:
 | 
			
		||||
            await asyncio.sleep(60)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        await reconnect.stop()
 | 
			
		||||
        zc.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_logs(config, address):
 | 
			
		||||
    asyncio.run(async_run_logs(config, address))
 | 
			
		||||
@@ -76,13 +76,13 @@ class CustomAPIDevice {
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Subscribe to the state of an entity from Home Assistant.
 | 
			
		||||
  /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
 | 
			
		||||
   *
 | 
			
		||||
   * Usage:
 | 
			
		||||
   *
 | 
			
		||||
   * ```cpp
 | 
			
		||||
   * void setup() override {
 | 
			
		||||
   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
 | 
			
		||||
   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
 | 
			
		||||
   * }
 | 
			
		||||
   *
 | 
			
		||||
   * void on_state_changed(std::string state) {
 | 
			
		||||
@@ -93,17 +93,19 @@ class CustomAPIDevice {
 | 
			
		||||
   * @tparam T The class type creating the service, automatically deduced from the function pointer.
 | 
			
		||||
   * @param callback The member function to call when the entity state changes.
 | 
			
		||||
   * @param entity_id The entity_id to track.
 | 
			
		||||
   * @param attribute The entity state attribute to track.
 | 
			
		||||
   */
 | 
			
		||||
  template<typename T>
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) {
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
 | 
			
		||||
                                     const std::string &attribute = "") {
 | 
			
		||||
    auto f = std::bind(callback, (T *) this, std::placeholders::_1);
 | 
			
		||||
    global_api_server->subscribe_home_assistant_state(entity_id, f);
 | 
			
		||||
    global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Subscribe to the state of an entity from Home Assistant.
 | 
			
		||||
  /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
 | 
			
		||||
   *
 | 
			
		||||
   * Usage:
 | 
			
		||||
   *
 | 
			
		||||
   *å
 | 
			
		||||
   * ```cpp
 | 
			
		||||
   * void setup() override {
 | 
			
		||||
   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
 | 
			
		||||
@@ -117,11 +119,13 @@ class CustomAPIDevice {
 | 
			
		||||
   * @tparam T The class type creating the service, automatically deduced from the function pointer.
 | 
			
		||||
   * @param callback The member function to call when the entity state changes.
 | 
			
		||||
   * @param entity_id The entity_id to track.
 | 
			
		||||
   * @param attribute The entity state attribute to track.
 | 
			
		||||
   */
 | 
			
		||||
  template<typename T>
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) {
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
 | 
			
		||||
                                     const std::string &attribute = "") {
 | 
			
		||||
    auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
 | 
			
		||||
    global_api_server->subscribe_home_assistant_state(entity_id, f);
 | 
			
		||||
    global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Call a Home Assistant service from ESPHome.
 | 
			
		||||
 
 | 
			
		||||
@@ -51,5 +51,13 @@ bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
 | 
			
		||||
bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,12 @@ class ListEntitiesIterator : public ComponentIterator {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  bool on_climate(climate::Climate *climate) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool on_number(number::Number *number) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool on_select(select::Select *select) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool on_end() override;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "api.proto";
 | 
			
		||||
static const char *const TAG = "api.proto";
 | 
			
		||||
 | 
			
		||||
void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
 | 
			
		||||
  uint32_t i = 0;
 | 
			
		||||
@@ -80,11 +80,13 @@ void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
std::string ProtoMessage::dump() const {
 | 
			
		||||
  std::string out;
 | 
			
		||||
  this->dump_to(out);
 | 
			
		||||
  return out;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,13 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
 | 
			
		||||
#define HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
@@ -243,8 +248,10 @@ class ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void encode(ProtoWriteBuffer buffer) const = 0;
 | 
			
		||||
  void decode(const uint8_t *buffer, size_t length);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  std::string dump() const;
 | 
			
		||||
  virtual void dump_to(std::string &out) const = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,16 @@ bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor)
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
bool InitialStateIterator::on_number(number::Number *number) {
 | 
			
		||||
  return this->client_->send_number_state(number, number->state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
bool InitialStateIterator::on_select(select::Select *select) {
 | 
			
		||||
  return this->client_->send_select_state(select, select->state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
 | 
			
		||||
    : ComponentIterator(server), client_(client) {}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,12 @@ class InitialStateIterator : public ComponentIterator {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  bool on_climate(climate::Climate *climate) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool on_number(number::Number *number) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool on_select(select::Select *select) override;
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  APIConnection *client_;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceAr
 | 
			
		||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.bool_array;
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<int> get_execute_arg_value<std::vector<int>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.int_array;
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user