mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 00:21:56 +00:00 
			
		
		
		
	Compare commits
	
		
			403 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b91ee4847f | ||
| 
						 | 
					625463d871 | ||
| 
						 | 
					6433a01e07 | ||
| 
						 | 
					56cc31e8e7 | ||
| 
						 | 
					3af297aa76 | ||
| 
						 | 
					996ec59d28 | ||
| 
						 | 
					95593eeeab | ||
| 
						 | 
					dad244fb7a | ||
| 
						 | 
					adb5d27d95 | ||
| 
						 | 
					8456a8cecb | ||
| 
						 | 
					b9f66373c1 | ||
| 
						 | 
					9ac365feef | ||
| 
						 | 
					43bbd58a44 | ||
| 
						 | 
					7feffa64f3 | ||
| 
						 | 
					ea0977abb4 | ||
| 
						 | 
					4c83dc7c28 | ||
| 
						 | 
					e10ab1da78 | ||
| 
						 | 
					1b0e60374b | ||
| 
						 | 
					3a760fbb44 | ||
| 
						 | 
					6ef57a2973 | ||
| 
						 | 
					3e9c7f2e9f | ||
| 
						 | 
					430598b7a1 | ||
| 
						 | 
					91611b09b4 | ||
| 
						 | 
					ecd115851f | ||
| 
						 | 
					4a1e50fed1 | ||
| 
						 | 
					d6d037047b | ||
| 
						 | 
					b5734c2b20 | ||
| 
						 | 
					723fb7eaac | ||
| 
						 | 
					63a9acaa19 | ||
| 
						 | 
					0524f8c677 | ||
| 
						 | 
					70b62f272e | ||
| 
						 | 
					f0089b7940 | ||
| 
						 | 
					4b44280d53 | ||
| 
						 | 
					f045382d20 | ||
| 
						 | 
					db3fa1ade7 | ||
| 
						 | 
					f83950fd75 | ||
| 
						 | 
					4dd1bf920d | ||
| 
						 | 
					98755f3621 | ||
| 
						 | 
					c3a8a044b9 | ||
| 
						 | 
					15b5ea43a7 | ||
| 
						 | 
					ec683fc227 | ||
| 
						 | 
					d4e65eb82a | ||
| 
						 | 
					10c6601b0a | ||
| 
						 | 
					73940bc1bd | ||
| 
						 | 
					9b7fb829f9 | ||
| 
						 | 
					c51d8c9021 | ||
| 
						 | 
					d8a6dfe5ce | ||
| 
						 | 
					5f7cef0b06 | ||
| 
						 | 
					48ff2ffc68 | ||
| 
						 | 
					b3b9ccd314 | ||
| 
						 | 
					e63c7b483b | ||
| 
						 | 
					f57980b069 | ||
| 
						 | 
					7006aa0d2a | ||
| 
						 | 
					bb86db869a | ||
| 
						 | 
					4c4dd23e15 | ||
| 
						 | 
					8051c1ca99 | ||
| 
						 | 
					fe5a6847b5 | ||
| 
						 | 
					a779592414 | ||
| 
						 | 
					112215848d | ||
| 
						 | 
					3dee057826 | ||
| 
						 | 
					4406a08fa7 | ||
| 
						 | 
					c33077bc61 | ||
| 
						 | 
					34db9d9ef2 | ||
| 
						 | 
					1184bbc976 | ||
| 
						 | 
					a3eb2a7ee0 | ||
| 
						 | 
					d13134135b | ||
| 
						 | 
					b4f57972fb | ||
| 
						 | 
					6a5eb43454 | ||
| 
						 | 
					04ec1c8b56 | ||
| 
						 | 
					d7ad155885 | ||
| 
						 | 
					85461a752a | ||
| 
						 | 
					039fbc677d | ||
| 
						 | 
					ea56a39e11 | ||
| 
						 | 
					55e9560e74 | ||
| 
						 | 
					3cb4b4ca03 | ||
| 
						 | 
					11d2866755 | ||
| 
						 | 
					2c517e3e8c | ||
| 
						 | 
					42739f0b22 | ||
| 
						 | 
					a1f9b0d7f2 | ||
| 
						 | 
					c3b8c84131 | ||
| 
						 | 
					471b82f727 | ||
| 
						 | 
					92b85f98e8 | ||
| 
						 | 
					c092d92d45 | ||
| 
						 | 
					e514a1fcd4 | ||
| 
						 | 
					a1b28cb36e | ||
| 
						 | 
					3f2d9abfe6 | ||
| 
						 | 
					f3ec4b514d | ||
| 
						 | 
					fc5798fa71 | ||
| 
						 | 
					95d7ad543f | ||
| 
						 | 
					d9b2903d78 | ||
| 
						 | 
					32a664eedc | ||
| 
						 | 
					e7477890cf | ||
| 
						 | 
					9bf72ff05f | ||
| 
						 | 
					5461f87ff0 | ||
| 
						 | 
					1c58b17235 | ||
| 
						 | 
					d34a1c3ed6 | ||
| 
						 | 
					22e3bc7cfe | ||
| 
						 | 
					955c96731e | ||
| 
						 | 
					54a173dbf1 | ||
| 
						 | 
					9ff8240802 | ||
| 
						 | 
					7bbb5213f3 | ||
| 
						 | 
					b8b30599ee | ||
| 
						 | 
					e083d7f4d0 | ||
| 
						 | 
					a57580b5ab | ||
| 
						 | 
					e22f1fc044 | ||
| 
						 | 
					e09ee8f23d | ||
| 
						 | 
					6ec546a6a4 | ||
| 
						 | 
					877367677b | ||
| 
						 | 
					8be4086224 | ||
| 
						 | 
					871d3b66fb | ||
| 
						 | 
					87358e8843 | ||
| 
						 | 
					5c06cd8eb3 | ||
| 
						 | 
					46b4c970d1 | ||
| 
						 | 
					49f46a7cdd | ||
| 
						 | 
					1627dff166 | ||
| 
						 | 
					cee08debff | ||
| 
						 | 
					912793eddf | ||
| 
						 | 
					eaa5200a35 | ||
| 
						 | 
					a7687c3e17 | ||
| 
						 | 
					932e0469f7 | ||
| 
						 | 
					15ab8918af | ||
| 
						 | 
					d0dfc94a61 | ||
| 
						 | 
					5a2984d03a | ||
| 
						 | 
					c89018a431 | ||
| 
						 | 
					1031ea4313 | ||
| 
						 | 
					5b0fbbaada | ||
| 
						 | 
					0e4f1ac40d | ||
| 
						 | 
					946db3fd50 | ||
| 
						 | 
					3dfc8d4291 | ||
| 
						 | 
					4f5e4f3b86 | ||
| 
						 | 
					505d1d78fb | ||
| 
						 | 
					7af1c04493 | ||
| 
						 | 
					855c98d815 | ||
| 
						 | 
					c26ea7e4e0 | ||
| 
						 | 
					c39ac9edfe | ||
| 
						 | 
					af04f565cf | ||
| 
						 | 
					2b9054d3b2 | ||
| 
						 | 
					c83ecf764d | ||
| 
						 | 
					a2485a18cb | ||
| 
						 | 
					8ef2ad17b5 | ||
| 
						 | 
					4579f78bf9 | ||
| 
						 | 
					1853407645 | ||
| 
						 | 
					cb5efc1c42 | ||
| 
						 | 
					2234f6aacf | ||
| 
						 | 
					be965a60eb | ||
| 
						 | 
					5596751c2c | ||
| 
						 | 
					6417d8132d | ||
| 
						 | 
					5624fafb3a | ||
| 
						 | 
					2eb5f89d82 | ||
| 
						 | 
					e30f17f64f | ||
| 
						 | 
					1ba560dc9e | ||
| 
						 | 
					8c86a18dc6 | ||
| 
						 | 
					b2d516c70a | ||
| 
						 | 
					45940b0514 | ||
| 
						 | 
					97e76d64d6 | ||
| 
						 | 
					756c6721e9 | ||
| 
						 | 
					4c390d9f9f | ||
| 
						 | 
					0d0954d74b | ||
| 
						 | 
					7672ba2c8d | ||
| 
						 | 
					4d28afc153 | ||
| 
						 | 
					7246f42a8e | ||
| 
						 | 
					bdcffc7ba9 | ||
| 
						 | 
					95a6715b2b | ||
| 
						 | 
					5342edf04a | ||
| 
						 | 
					d344b1ca0e | ||
| 
						 | 
					278863d027 | ||
| 
						 | 
					8503e08ee6 | ||
| 
						 | 
					aec02afcdc | ||
| 
						 | 
					52dd79691b | ||
| 
						 | 
					aea2491fa4 | ||
| 
						 | 
					963b28181f | ||
| 
						 | 
					210a9a4162 | ||
| 
						 | 
					a27a884191 | ||
| 
						 | 
					17dcba8f8a | ||
| 
						 | 
					ea6a7a22ff | ||
| 
						 | 
					5ddba719c5 | ||
| 
						 | 
					b398d826c1 | ||
| 
						 | 
					edb557f79e | ||
| 
						 | 
					f463cd98f8 | ||
| 
						 | 
					262d69308d | ||
| 
						 | 
					0406e27100 | ||
| 
						 | 
					ed3ad615d8 | ||
| 
						 | 
					66761ff340 | ||
| 
						 | 
					8bebf138ee | ||
| 
						 | 
					fd836e982e | ||
| 
						 | 
					e32722db70 | ||
| 
						 | 
					b20760c93c | ||
| 
						 | 
					654e31124e | ||
| 
						 | 
					8e36e1b92e | ||
| 
						 | 
					9fe7b08874 | ||
| 
						 | 
					f1364d4af4 | ||
| 
						 | 
					ed593544d8 | ||
| 
						 | 
					0929a0f8aa | ||
| 
						 | 
					13b3412b45 | ||
| 
						 | 
					888e315553 | ||
| 
						 | 
					11daabc9c2 | ||
| 
						 | 
					40e0100c1e | ||
| 
						 | 
					c51352d04d | ||
| 
						 | 
					c8a8acd46e | ||
| 
						 | 
					bbac1534a3 | ||
| 
						 | 
					637b55bfbf | ||
| 
						 | 
					92a24d52be | ||
| 
						 | 
					491f8cc611 | ||
| 
						 | 
					71fc61117b | ||
| 
						 | 
					24f445dade | ||
| 
						 | 
					7c884329eb | ||
| 
						 | 
					bac58bba4d | ||
| 
						 | 
					250bf3f054 | ||
| 
						 | 
					e65a7d887f | ||
| 
						 | 
					ac0d921413 | ||
| 
						 | 
					1e8e471dec | ||
| 
						 | 
					2d7f8b3bdf | ||
| 
						 | 
					7452ef23b1 | ||
| 
						 | 
					9ebe075f9b | ||
| 
						 | 
					3052c64dd7 | ||
| 
						 | 
					5e345783bd | ||
| 
						 | 
					81685573e1 | ||
| 
						 | 
					945ed5d3bd | ||
| 
						 | 
					fff5ba03c2 | ||
| 
						 | 
					82eca13d7b | ||
| 
						 | 
					5f21b925da | ||
| 
						 | 
					272ceadbb0 | ||
| 
						 | 
					d26c2b1a44 | ||
| 
						 | 
					8bda8e5393 | ||
| 
						 | 
					954b8a0cff | ||
| 
						 | 
					7c17e72db4 | ||
| 
						 | 
					d180aee57f | ||
| 
						 | 
					e3ffecefc0 | ||
| 
						 | 
					4c61cf153c | ||
| 
						 | 
					c78fb90e2f | ||
| 
						 | 
					a990898256 | ||
| 
						 | 
					c60c618204 | ||
| 
						 | 
					53bd197c44 | ||
| 
						 | 
					dbb195691b | ||
| 
						 | 
					50da630811 | ||
| 
						 | 
					30eca885c9 | ||
| 
						 | 
					f76685fccf | ||
| 
						 | 
					68d547595e | ||
| 
						 | 
					64341d1d18 | ||
| 
						 | 
					2e49039c01 | ||
| 
						 | 
					321504cf29 | ||
| 
						 | 
					0f4a7bf1f5 | ||
| 
						 | 
					711e74a12b | ||
| 
						 | 
					8f3a739da7 | ||
| 
						 | 
					aed140d802 | ||
| 
						 | 
					c69b88bb55 | ||
| 
						 | 
					c6dc8a11e2 | ||
| 
						 | 
					6366ff6421 | ||
| 
						 | 
					607ddaa632 | ||
| 
						 | 
					d281e59f3a | ||
| 
						 | 
					2db8c42e1d | ||
| 
						 | 
					aa8eb2c92a | ||
| 
						 | 
					b422a63b2a | ||
| 
						 | 
					ad5f2cd748 | ||
| 
						 | 
					efae363739 | ||
| 
						 | 
					2d79d21c50 | ||
| 
						 | 
					3b9d126322 | ||
| 
						 | 
					0ea77de98c | ||
| 
						 | 
					19014331d8 | ||
| 
						 | 
					b276ac0588 | ||
| 
						 | 
					de33cbd7e7 | ||
| 
						 | 
					103ba4c696 | ||
| 
						 | 
					5a90b83f63 | ||
| 
						 | 
					716039e452 | ||
| 
						 | 
					896654aaef | ||
| 
						 | 
					5fad38f65f | ||
| 
						 | 
					89f2ea5725 | ||
| 
						 | 
					a32ad33b4e | ||
| 
						 | 
					a328fff5a7 | ||
| 
						 | 
					233783c76c | ||
| 
						 | 
					39a18fb358 | ||
| 
						 | 
					460a144ca8 | ||
| 
						 | 
					23ead416d5 | ||
| 
						 | 
					1b5f11bbee | ||
| 
						 | 
					4cc2817fcd | ||
| 
						 | 
					d437cc915c | ||
| 
						 | 
					dd3f2f6c7e | ||
| 
						 | 
					5f61897bec | ||
| 
						 | 
					c5d26a5b4a | ||
| 
						 | 
					855112dfc3 | ||
| 
						 | 
					0da97289e6 | ||
| 
						 | 
					b9767bdcbc | ||
| 
						 | 
					91f12a50cf | ||
| 
						 | 
					e92a9d1d9e | ||
| 
						 | 
					4eb51ab4d6 | ||
| 
						 | 
					e6b0a0ca2b | ||
| 
						 | 
					924df1e7de | ||
| 
						 | 
					ed7983af41 | ||
| 
						 | 
					40c474cd83 | ||
| 
						 | 
					a2d2863c72 | ||
| 
						 | 
					133a17d6eb | ||
| 
						 | 
					fe47ddc27a | ||
| 
						 | 
					aad03f1bf5 | ||
| 
						 | 
					a4867a00ea | ||
| 
						 | 
					e0cff214b2 | ||
| 
						 | 
					c6109024aa | ||
| 
						 | 
					4e308f551c | ||
| 
						 | 
					8a2b1d9359 | ||
| 
						 | 
					63a186bdf9 | ||
| 
						 | 
					d594a6fcbc | ||
| 
						 | 
					e18dfdd656 | ||
| 
						 | 
					3aa107142b | ||
| 
						 | 
					0cd24c629a | ||
| 
						 | 
					f31e0532c4 | ||
| 
						 | 
					f0b6aabc96 | ||
| 
						 | 
					97a18717e6 | ||
| 
						 | 
					ede1de9021 | ||
| 
						 | 
					f1a8d957f8 | ||
| 
						 | 
					9821a3442b | ||
| 
						 | 
					87842e097b | ||
| 
						 | 
					7dd40e2014 | ||
| 
						 | 
					3d71e2e189 | ||
| 
						 | 
					affaaf7d2c | ||
| 
						 | 
					a719998220 | ||
| 
						 | 
					bad161a5c1 | ||
| 
						 | 
					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 | ||
| 
						 | 
					ed68a0e773 | ||
| 
						 | 
					e2640c8368 | ||
| 
						 | 
					eff626248f | ||
| 
						 | 
					ce29a3b07a | ||
| 
						 | 
					1b89174558 | ||
| 
						 | 
					1c1ad32610 | ||
| 
						 | 
					71237e2f76 | ||
| 
						 | 
					518c271eba | ||
| 
						 | 
					d71996e58d | ||
| 
						 | 
					2f33cd2db5 | ||
| 
						 | 
					5ec9bb0fb5 | ||
| 
						 | 
					8cc3cbb22e | ||
| 
						 | 
					b0fa317302 | ||
| 
						 | 
					5cb56bc677 | ||
| 
						 | 
					21f8fd9fa5 | ||
| 
						 | 
					2100ef63a9 | ||
| 
						 | 
					29db77c9c9 | ||
| 
						 | 
					f0b14055b6 | ||
| 
						 | 
					fbd9e87b51 | ||
| 
						 | 
					edb3b77916 | ||
| 
						 | 
					ebaa84617f | ||
| 
						 | 
					8eb18995cb | ||
| 
						 | 
					ebabf0e7d8 | ||
| 
						 | 
					607e1f823d | ||
| 
						 | 
					3b52a306cd | ||
| 
						 | 
					0c370d5897 | ||
| 
						 | 
					9b48ff5775 | ||
| 
						 | 
					117b58ebe6 | ||
| 
						 | 
					303b699005 | ||
| 
						 | 
					9173da0416 | 
							
								
								
									
										27
									
								
								.clang-tidy
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.clang-tidy
									
									
									
									
									
								
							@@ -2,9 +2,11 @@
 | 
			
		||||
Checks: >-
 | 
			
		||||
  *,
 | 
			
		||||
  -abseil-*,
 | 
			
		||||
  -altera-*,
 | 
			
		||||
  -android-*,
 | 
			
		||||
  -boost-*,
 | 
			
		||||
  -bugprone-branch-clone,
 | 
			
		||||
  -bugprone-easily-swappable-parameters,
 | 
			
		||||
  -bugprone-narrowing-conversions,
 | 
			
		||||
  -bugprone-signed-char-misuse,
 | 
			
		||||
  -bugprone-too-small-loop-variable,
 | 
			
		||||
@@ -20,6 +22,7 @@ Checks: >-
 | 
			
		||||
  -clang-diagnostic-sign-compare,
 | 
			
		||||
  -clang-diagnostic-unused-variable,
 | 
			
		||||
  -clang-diagnostic-unused-const-variable,
 | 
			
		||||
  -concurrency-*,
 | 
			
		||||
  -cppcoreguidelines-avoid-c-arrays,
 | 
			
		||||
  -cppcoreguidelines-avoid-goto,
 | 
			
		||||
  -cppcoreguidelines-avoid-magic-numbers,
 | 
			
		||||
@@ -27,7 +30,6 @@ Checks: >-
 | 
			
		||||
  -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,
 | 
			
		||||
  -cppcoreguidelines-pro-bounds-pointer-arithmetic,
 | 
			
		||||
@@ -61,17 +63,21 @@ Checks: >-
 | 
			
		||||
  -misc-no-recursion,
 | 
			
		||||
  -misc-unused-parameters,
 | 
			
		||||
  -modernize-avoid-c-arrays,
 | 
			
		||||
  -modernize-avoid-bind,
 | 
			
		||||
  -modernize-concat-nested-namespaces,
 | 
			
		||||
  -modernize-return-braced-init-list,
 | 
			
		||||
  -modernize-use-auto,
 | 
			
		||||
  -modernize-use-default-member-init,
 | 
			
		||||
  -modernize-use-equals-default,
 | 
			
		||||
  -modernize-use-trailing-return-type,
 | 
			
		||||
  -modernize-use-nodiscard,
 | 
			
		||||
  -mpi-*,
 | 
			
		||||
  -objc-*,
 | 
			
		||||
  -readability-braces-around-statements,
 | 
			
		||||
  -readability-const-return-type,
 | 
			
		||||
  -readability-convert-member-functions-to-static,
 | 
			
		||||
  -readability-else-after-return,
 | 
			
		||||
  -readability-function-cognitive-complexity,
 | 
			
		||||
  -readability-implicit-bool-conversion,
 | 
			
		||||
  -readability-isolate-declaration,
 | 
			
		||||
  -readability-magic-numbers,
 | 
			
		||||
@@ -83,7 +89,6 @@ Checks: >-
 | 
			
		||||
  -readability-redundant-string-init,
 | 
			
		||||
  -readability-uppercase-literal-suffix,
 | 
			
		||||
  -readability-use-anyofallof,
 | 
			
		||||
  -warnings-as-errors
 | 
			
		||||
WarningsAsErrors: '*'
 | 
			
		||||
AnalyzeTemporaryDtors: false
 | 
			
		||||
FormatStyle:     google
 | 
			
		||||
@@ -108,6 +113,10 @@ CheckOptions:
 | 
			
		||||
    value:           llvm
 | 
			
		||||
  - key:             modernize-use-nullptr.NullMacros
 | 
			
		||||
    value:           'NULL'
 | 
			
		||||
  - key:             modernize-make-unique.MakeSmartPtrFunction
 | 
			
		||||
    value:           'make_unique'
 | 
			
		||||
  - key:             modernize-make-unique.MakeSmartPtrFunctionHeader
 | 
			
		||||
    value:           'esphome/core/helpers.h'
 | 
			
		||||
  - key:             readability-identifier-naming.LocalVariableCase
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.ClassCase
 | 
			
		||||
@@ -121,15 +130,19 @@ CheckOptions:
 | 
			
		||||
  - key:             readability-identifier-naming.StaticConstantCase
 | 
			
		||||
    value:           'UPPER_CASE'
 | 
			
		||||
  - key:             readability-identifier-naming.StaticVariableCase
 | 
			
		||||
    value:           'UPPER_CASE'
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.GlobalConstantCase
 | 
			
		||||
    value:           'UPPER_CASE'
 | 
			
		||||
  - key:             readability-identifier-naming.ParameterCase
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMemberPrefix
 | 
			
		||||
    value:           'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMethodPrefix
 | 
			
		||||
    value:           'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMemberCase
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMemberSuffix
 | 
			
		||||
    value:           '_'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMethodCase
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.PrivateMethodSuffix
 | 
			
		||||
    value:           '_'
 | 
			
		||||
  - key:             readability-identifier-naming.ClassMemberCase
 | 
			
		||||
    value:           'lower_case'
 | 
			
		||||
  - key:             readability-identifier-naming.ClassMemberCase
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ESPHome Dev",
 | 
			
		||||
  "context": "..",
 | 
			
		||||
  "dockerFile": "../docker/Dockerfile.dev",
 | 
			
		||||
  "image": "esphome/esphome-lint:dev",
 | 
			
		||||
  "postCreateCommand": [
 | 
			
		||||
    "script/devcontainer-post-create"
 | 
			
		||||
  ],
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
# Normalize line endings to LF in the repository
 | 
			
		||||
* text eol=lf
 | 
			
		||||
							
								
								
									
										59
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,59 +0,0 @@
 | 
			
		||||
# Configuration for probot-stale - https://github.com/probot/stale
 | 
			
		||||
 | 
			
		||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
 | 
			
		||||
daysUntilStale: 60
 | 
			
		||||
 | 
			
		||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
 | 
			
		||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
 | 
			
		||||
daysUntilClose: 7
 | 
			
		||||
 | 
			
		||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
 | 
			
		||||
onlyLabels: []
 | 
			
		||||
 | 
			
		||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
 | 
			
		||||
exemptLabels:
 | 
			
		||||
  - not-stale
 | 
			
		||||
 | 
			
		||||
# Set to true to ignore issues in a project (defaults to false)
 | 
			
		||||
exemptProjects: false
 | 
			
		||||
 | 
			
		||||
# Set to true to ignore issues in a milestone (defaults to false)
 | 
			
		||||
exemptMilestones: true
 | 
			
		||||
 | 
			
		||||
# Set to true to ignore issues with an assignee (defaults to false)
 | 
			
		||||
exemptAssignees: false
 | 
			
		||||
 | 
			
		||||
# Label to use when marking as stale
 | 
			
		||||
staleLabel: stale
 | 
			
		||||
 | 
			
		||||
# Comment to post when marking as stale. Set to `false` to disable
 | 
			
		||||
markComment: >
 | 
			
		||||
  This issue has been automatically marked as stale because it has not had
 | 
			
		||||
  recent activity. It will be closed if no further activity occurs. Thank you
 | 
			
		||||
  for your contributions.
 | 
			
		||||
 | 
			
		||||
# Comment to post when removing the stale label.
 | 
			
		||||
# unmarkComment: >
 | 
			
		||||
#   Your comment here.
 | 
			
		||||
 | 
			
		||||
# Comment to post when closing a stale Issue or Pull Request.
 | 
			
		||||
# closeComment: >
 | 
			
		||||
#   Your comment here.
 | 
			
		||||
 | 
			
		||||
# Limit the number of actions per hour, from 1-30. Default is 30
 | 
			
		||||
limitPerRun: 10
 | 
			
		||||
 | 
			
		||||
# Limit to only `issues` or `pulls`
 | 
			
		||||
only: pulls
 | 
			
		||||
 | 
			
		||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
 | 
			
		||||
# pulls:
 | 
			
		||||
#   daysUntilStale: 30
 | 
			
		||||
#   markComment: >
 | 
			
		||||
#     This pull request has been automatically marked as stale because it has not had
 | 
			
		||||
#     recent activity. It will be closed if no further activity occurs. Thank you
 | 
			
		||||
#     for your contributions.
 | 
			
		||||
 | 
			
		||||
# issues:
 | 
			
		||||
#   exemptLabels:
 | 
			
		||||
#     - confirmed
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -7,11 +7,15 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docker/**'
 | 
			
		||||
      - '.github/workflows/**'
 | 
			
		||||
      - 'requirements*.txt'
 | 
			
		||||
      - 'platformio.ini'
 | 
			
		||||
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docker/**'
 | 
			
		||||
      - '.github/workflows/**'
 | 
			
		||||
      - 'requirements*.txt'
 | 
			
		||||
      - 'platformio.ini'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  check-docker:
 | 
			
		||||
@@ -27,6 +31,11 @@ jobs:
 | 
			
		||||
      uses: actions/setup-python@v2
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
    - name: Set up Docker Buildx
 | 
			
		||||
      uses: docker/setup-buildx-action@v1
 | 
			
		||||
    - name: Set up QEMU
 | 
			
		||||
      uses: docker/setup-qemu-action@v1
 | 
			
		||||
 | 
			
		||||
    - name: Set TAG
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "TAG=check" >> $GITHUB_ENV
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										151
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										151
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -9,58 +9,7 @@ on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  ci-with-container:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
      - 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 }}
 | 
			
		||||
        if: ${{ matrix.id == 'clang-tidy' }}
 | 
			
		||||
 | 
			
		||||
      - 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:
 | 
			
		||||
@@ -74,48 +23,87 @@ jobs:
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test1.yaml
 | 
			
		||||
            name: Test tests/test1.yaml
 | 
			
		||||
            pio_cache_key: test1
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test2.yaml
 | 
			
		||||
            name: Test tests/test2.yaml
 | 
			
		||||
            pio_cache_key: test2
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test3.yaml
 | 
			
		||||
            name: Test tests/test3.yaml
 | 
			
		||||
            pio_cache_key: test1
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test4.yaml
 | 
			
		||||
            name: Test tests/test4.yaml
 | 
			
		||||
            pio_cache_key: test4
 | 
			
		||||
          - id: test
 | 
			
		||||
            file: tests/test5.yaml
 | 
			
		||||
            name: Test tests/test5.yaml
 | 
			
		||||
            pio_cache_key: test5
 | 
			
		||||
          - id: pytest
 | 
			
		||||
            name: Run pytest
 | 
			
		||||
          - id: clang-format
 | 
			
		||||
            name: Run script/clang-format
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP8266
 | 
			
		||||
            options: --environment esp8266-tidy --grep USE_ESP8266
 | 
			
		||||
            pio_cache_key: tidyesp8266
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 1/4
 | 
			
		||||
            options: --environment esp32-tidy --split-num 4 --split-at 1
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 2/4
 | 
			
		||||
            options: --environment esp32-tidy --split-num 4 --split-at 2
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 3/4
 | 
			
		||||
            options: --environment esp32-tidy --split-num 4 --split-at 3
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 4/4
 | 
			
		||||
            options: --environment esp32-tidy --split-num 4 --split-at 4
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 esp-idf
 | 
			
		||||
            options: --environment esp32-idf-tidy --grep USE_ESP_IDF
 | 
			
		||||
            pio_cache_key: tidyesp32-idf
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        id: python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
 | 
			
		||||
      - name: Cache pip modules
 | 
			
		||||
        uses: actions/cache@v1
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.cache/pip
 | 
			
		||||
          key: esphome-pip-3.7-${{ hashFiles('setup.py') }}
 | 
			
		||||
          key: pip-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }}
 | 
			
		||||
          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' }}
 | 
			
		||||
            pip-${{ steps.python.outputs.python-version }}-
 | 
			
		||||
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        run: script/setup
 | 
			
		||||
        run: |
 | 
			
		||||
          pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
          pip3 install -e .
 | 
			
		||||
 | 
			
		||||
      # Use per check platformio cache because checks use different parts
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
        if: matrix.id == 'test' || matrix.id == 'clang-tidy'
 | 
			
		||||
 | 
			
		||||
      - name: Install clang tools
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get install \
 | 
			
		||||
              clang-format-11 \
 | 
			
		||||
              clang-tidy-11
 | 
			
		||||
        if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format'
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -124,20 +112,45 @@ jobs:
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/python.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/pytest.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Lint Custom
 | 
			
		||||
        run: |
 | 
			
		||||
          script/ci-custom.py
 | 
			
		||||
          script/build_codeowners.py --check
 | 
			
		||||
        if: ${{ matrix.id == 'ci-custom' }}
 | 
			
		||||
        if: matrix.id == 'ci-custom'
 | 
			
		||||
 | 
			
		||||
      - name: Lint Python
 | 
			
		||||
        run: script/lint-python
 | 
			
		||||
        if: ${{ matrix.id == 'lint-python' }}
 | 
			
		||||
        if: matrix.id == 'lint-python'
 | 
			
		||||
 | 
			
		||||
      - run: esphome compile ${{ matrix.file }}
 | 
			
		||||
        if: ${{ matrix.id == 'test' }}
 | 
			
		||||
        if: matrix.id == 'test'
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        run: |
 | 
			
		||||
          pytest -vv --tb=native tests
 | 
			
		||||
        if: ${{ matrix.id == 'pytest' }}
 | 
			
		||||
        if: matrix.id == 'pytest'
 | 
			
		||||
 | 
			
		||||
      # 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 ${{ matrix.options }}
 | 
			
		||||
        if: matrix.id == 'clang-tidy'
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										100
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,100 +0,0 @@
 | 
			
		||||
name: Build and publish lint docker image
 | 
			
		||||
 | 
			
		||||
# Only run when docker paths change
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [dev]
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docker/Dockerfile.lint'
 | 
			
		||||
      - 'requirements.txt'
 | 
			
		||||
      - 'requirements_optional.txt'
 | 
			
		||||
      - 'requirements_test.txt'
 | 
			
		||||
      - 'platformio.ini'
 | 
			
		||||
      - '.github/workflows/docker-lint-build.yml'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  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: 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
 | 
			
		||||
							
								
								
									
										21
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
name: Lock
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '30 0 * * *'
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  issues: write
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lock:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: dessant/lock-threads@v2
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ github.token }}
 | 
			
		||||
          pr-lock-inactive-days: "1"
 | 
			
		||||
          pr-lock-reason: ""
 | 
			
		||||
          process-only: prs
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -57,7 +57,7 @@ jobs:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        build_type: ["ha-addon", "docker"]
 | 
			
		||||
        build_type: ["ha-addon", "docker", "lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
@@ -65,13 +65,10 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: '3.9'
 | 
			
		||||
 | 
			
		||||
    - name: Run build
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          build
 | 
			
		||||
    - name: Set up Docker Buildx
 | 
			
		||||
      uses: docker/setup-buildx-action@v1
 | 
			
		||||
    - name: Set up QEMU
 | 
			
		||||
      uses: docker/setup-qemu-action@v1
 | 
			
		||||
 | 
			
		||||
    - name: Log in to docker hub
 | 
			
		||||
      uses: docker/login-action@v1
 | 
			
		||||
@@ -85,13 +82,14 @@ jobs:
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
    - name: Run push
 | 
			
		||||
    - name: Build and push
 | 
			
		||||
      run: |
 | 
			
		||||
        docker/build.py \
 | 
			
		||||
          --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
          --arch "${{ matrix.arch }}" \
 | 
			
		||||
          --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
          push
 | 
			
		||||
          build \
 | 
			
		||||
          --push
 | 
			
		||||
 | 
			
		||||
  deploy-docker-manifest:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
@@ -99,7 +97,7 @@ jobs:
 | 
			
		||||
    needs: [init, deploy-docker]
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        build_type: ["ha-addon", "docker"]
 | 
			
		||||
        build_type: ["ha-addon", "docker", "lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set up Python
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
name: Stale
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '30 0 * * *'
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  issues: write
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  stale:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v4
 | 
			
		||||
        with:
 | 
			
		||||
          repo-token: ${{ github.token }}
 | 
			
		||||
          days-before-pr-stale: 90
 | 
			
		||||
          days-before-pr-close: 7
 | 
			
		||||
          days-before-issue-stale: -1
 | 
			
		||||
          days-before-issue-close: -1
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
          stale-pr-label: "stale"
 | 
			
		||||
          exempt-pr-labels: "no-stale"
 | 
			
		||||
          stale-pr-message: >
 | 
			
		||||
            There hasn't been any activity on this pull request recently. This
 | 
			
		||||
            pull request has been automatically marked as stale because of that
 | 
			
		||||
            and will be closed if no further activity occurs within 7 days.
 | 
			
		||||
            Thank you for your contributions.
 | 
			
		||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -102,10 +102,7 @@ CMakeLists.txt
 | 
			
		||||
.idea/**/dynamic.xml
 | 
			
		||||
 | 
			
		||||
# CMake
 | 
			
		||||
cmake-build-debug/
 | 
			
		||||
cmake-build-livingroom8266/
 | 
			
		||||
cmake-build-livingroom32/
 | 
			
		||||
cmake-build-release/
 | 
			
		||||
cmake-build-*/
 | 
			
		||||
 | 
			
		||||
CMakeCache.txt
 | 
			
		||||
CMakeFiles
 | 
			
		||||
@@ -127,3 +124,6 @@ tests/.esphome/
 | 
			
		||||
/.temp-clang-tidy.cpp
 | 
			
		||||
/.temp/
 | 
			
		||||
.pio/
 | 
			
		||||
 | 
			
		||||
sdkconfig.*
 | 
			
		||||
!sdkconfig.defaults
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -14,6 +14,9 @@ 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_mini/* @ncareau
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
@@ -29,6 +32,7 @@ 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
 | 
			
		||||
@@ -36,14 +40,19 @@ esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
esphome/components/cs5460a/* @balrog-kun
 | 
			
		||||
esphome/components/ct_clamp/* @jesserockz
 | 
			
		||||
esphome/components/current_based/* @djwmarcx
 | 
			
		||||
esphome/components/daly_bms/* @s1lvi0
 | 
			
		||||
esphome/components/dashboard_import/* @esphome/core
 | 
			
		||||
esphome/components/debug/* @OttoWinter
 | 
			
		||||
esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dht/* @OttoWinter
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
			
		||||
esphome/components/esp32_improv/* @jesserockz
 | 
			
		||||
esphome/components/esp8266/* @esphome/core
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
esphome/components/fastled_base/* @OttoWinter
 | 
			
		||||
@@ -51,7 +60,11 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh
 | 
			
		||||
esphome/components/globals/* @esphome/core
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/graph/* @synco
 | 
			
		||||
esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter
 | 
			
		||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
 | 
			
		||||
@@ -65,6 +78,7 @@ esphome/components/json/* @OttoWinter
 | 
			
		||||
esphome/components/ledc/* @OttoWinter
 | 
			
		||||
esphome/components/light/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
esphome/components/ltr390/* @sjtrny
 | 
			
		||||
esphome/components/max7219digit/* @rspaargaren
 | 
			
		||||
esphome/components/mcp23008/* @jesserockz
 | 
			
		||||
esphome/components/mcp23017/* @jesserockz
 | 
			
		||||
@@ -75,9 +89,17 @@ 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/md5/* @esphome/core
 | 
			
		||||
esphome/components/mdns/* @esphome/core
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
esphome/components/modbus_controller/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/binary_sensor/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/number/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/output/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/sensor/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/switch/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/text_sensor/* @martgras
 | 
			
		||||
esphome/components/network/* @esphome/core
 | 
			
		||||
esphome/components/nextion/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/binary_sensor/* @senexcrenshaw
 | 
			
		||||
@@ -90,11 +112,13 @@ 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/preferences/* @esphome/core
 | 
			
		||||
esphome/components/pulse_meter/* @stevebaxter
 | 
			
		||||
esphome/components/pvvx_mithermometer/* @pasiz
 | 
			
		||||
esphome/components/rc522/* @glmnet
 | 
			
		||||
@@ -104,6 +128,8 @@ esphome/components/restart/* @esphome/core
 | 
			
		||||
esphome/components/rf_bridge/* @jesserockz
 | 
			
		||||
esphome/components/rgbct/* @jesserockz
 | 
			
		||||
esphome/components/rtttl/* @glmnet
 | 
			
		||||
esphome/components/safe_mode/* @paulmonigatti
 | 
			
		||||
esphome/components/scd4x/* @sjtrny
 | 
			
		||||
esphome/components/script/* @esphome/core
 | 
			
		||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
			
		||||
esphome/components/sdp3x/* @Azimath
 | 
			
		||||
@@ -115,6 +141,7 @@ 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
 | 
			
		||||
@@ -129,6 +156,7 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,6 @@
 | 
			
		||||
# Contributing to ESPHome
 | 
			
		||||
 | 
			
		||||
This python project is responsible for reading in YAML configuration files,
 | 
			
		||||
converting them to C++ code. This code is then converted to a platformio project and compiled
 | 
			
		||||
with [esphome-core](https://github.com/esphome/esphome-core), the C++ framework behind the project.
 | 
			
		||||
 | 
			
		||||
For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphomeyaml
 | 
			
		||||
For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome
 | 
			
		||||
 | 
			
		||||
Things to note when contributing:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,60 @@
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-base:latest
 | 
			
		||||
FROM ${BUILD_FROM}
 | 
			
		||||
# Build these with the build.py script
 | 
			
		||||
# Example:
 | 
			
		||||
#   python3 docker/build.py --tag dev --arch amd64 --build-type docker build
 | 
			
		||||
 | 
			
		||||
# One of "docker", "hassio"
 | 
			
		||||
ARG BASEIMGTYPE=docker
 | 
			
		||||
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7
 | 
			
		||||
FROM debian:bullseye-20210902-slim AS base-docker-amd64
 | 
			
		||||
FROM debian:bullseye-20210902-slim AS base-docker-arm64
 | 
			
		||||
FROM debian:bullseye-20210902-slim AS base-docker-armv7
 | 
			
		||||
 | 
			
		||||
# Use TARGETARCH/TARGETVARIANT defined by docker
 | 
			
		||||
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
 | 
			
		||||
FROM base-${BASEIMGTYPE}-${TARGETARCH}${TARGETVARIANT} AS base
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        python3=3.9.2-3 \
 | 
			
		||||
        python3-pip=20.3.4-4 \
 | 
			
		||||
        python3-setuptools=52.0.0-4 \
 | 
			
		||||
        python3-pil=8.1.2+dfsg-0.3 \
 | 
			
		||||
        python3-cryptography=3.3.2-1 \
 | 
			
		||||
        iputils-ping=3:20210202-1 \
 | 
			
		||||
        git=1:2.30.2-1 \
 | 
			
		||||
        curl=7.74.0-1.3+b1 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
 | 
			
		||||
  LANG=C.UTF-8 LC_ALL=C.UTF-8 \
 | 
			
		||||
  # Store globally installed pio libs in /piolibs
 | 
			
		||||
  PLATFORMIO_GLOBALLIB_DIR=/piolibs
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    # Ubuntu python3-pip is missing wheel
 | 
			
		||||
    pip3 install --no-cache-dir \
 | 
			
		||||
        wheel==0.36.2 \
 | 
			
		||||
        platformio==5.2.0 \
 | 
			
		||||
    # Change some platformio settings
 | 
			
		||||
    && platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_libraries_interval 1000000 \
 | 
			
		||||
    && platformio settings set check_platformio_interval 1000000 \
 | 
			
		||||
    && platformio settings set check_platforms_interval 1000000 \
 | 
			
		||||
    && mkdir -p /piolibs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= docker-type image =======================
 | 
			
		||||
FROM base AS docker
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
@@ -7,9 +62,9 @@ RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Then copy esphome and install
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN pip3 install --no-cache-dir -e .
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Settings for dashboard
 | 
			
		||||
ENV USERNAME="" PASSWORD=""
 | 
			
		||||
@@ -17,14 +72,85 @@ ENV USERNAME="" PASSWORD=""
 | 
			
		||||
# Expose the dashboard to Docker
 | 
			
		||||
EXPOSE 6052
 | 
			
		||||
 | 
			
		||||
# Run healthcheck (heartbeat)
 | 
			
		||||
HEALTHCHECK --interval=30s --timeout=30s \
 | 
			
		||||
  CMD curl --fail http://localhost:6052 || exit 1
 | 
			
		||||
COPY docker/docker_entrypoint.sh /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
# The directory the user should mount their configuration files to
 | 
			
		||||
VOLUME /config
 | 
			
		||||
WORKDIR /config
 | 
			
		||||
# Set entrypoint to esphome so that the user doesn't have to type 'esphome'
 | 
			
		||||
# Set entrypoint to esphome (via a script) so that the user doesn't have to type 'esphome'
 | 
			
		||||
# in every docker command twice
 | 
			
		||||
ENTRYPOINT ["esphome"]
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
# When no arguments given, start the dashboard in the workdir
 | 
			
		||||
CMD ["dashboard", "/config"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= hassio-type image =======================
 | 
			
		||||
FROM base AS hassio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        nginx=1.18.0-6.1 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
COPY docker/hassio-rootfs/ /
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
LABEL \
 | 
			
		||||
    io.hass.name="ESPHome" \
 | 
			
		||||
    io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
 | 
			
		||||
    io.hass.type="addon" \
 | 
			
		||||
    io.hass.version="${BUILD_VERSION}"
 | 
			
		||||
    # io.hass.arch is inherited from addon-debian-base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= lint-type image =======================
 | 
			
		||||
FROM base AS lint
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        clang-format-11=1:11.0.1-2 \
 | 
			
		||||
        clang-tidy-11=1:11.0.1-2 \
 | 
			
		||||
        patch=2.7.6-7 \
 | 
			
		||||
        software-properties-common=0.96.20.2-2.1 \
 | 
			
		||||
        nano=5.4-2 \
 | 
			
		||||
        build-essential=12.9 \
 | 
			
		||||
        python3-dev=3.9.2-3 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
WORKDIR /esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
FROM esphome/esphome-lint:1.1
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
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 requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
COPY docker/rootfs/ /
 | 
			
		||||
 | 
			
		||||
# Then copy esphome and install
 | 
			
		||||
COPY . /opt/esphome/
 | 
			
		||||
RUN pip3 install --no-cache-dir -e /opt/esphome
 | 
			
		||||
 | 
			
		||||
# Build arguments
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
LABEL \
 | 
			
		||||
    io.hass.name="ESPHome" \
 | 
			
		||||
    io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
 | 
			
		||||
    io.hass.type="addon" \
 | 
			
		||||
    io.hass.version=${BUILD_VERSION}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
ARG BUILD_FROM=esphome/esphome-lint-base:latest
 | 
			
		||||
FROM ${BUILD_FROM}
 | 
			
		||||
 | 
			
		||||
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_optional.txt -r /requirements_test.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
WORKDIR /esphome
 | 
			
		||||
							
								
								
									
										100
									
								
								docker/build.py
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								docker/build.py
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import subprocess
 | 
			
		||||
import argparse
 | 
			
		||||
import platform
 | 
			
		||||
from platform import machine
 | 
			
		||||
import shlex
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
@@ -24,9 +24,6 @@ TYPE_LINT = 'lint'
 | 
			
		||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_VERSION = "3.6.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")
 | 
			
		||||
@@ -34,27 +31,17 @@ parser.add_argument("--build-type", choices=TYPES, required=True, help="The type
 | 
			
		||||
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")
 | 
			
		||||
build_parser.add_argument("--push", help="Also push the images", action="store_true")
 | 
			
		||||
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
 | 
			
		||||
    baseimgtype: str
 | 
			
		||||
    platform: str
 | 
			
		||||
    target: str
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def for_type_arch(cls, build_type, arch):
 | 
			
		||||
@@ -63,18 +50,28 @@ class DockerParams:
 | 
			
		||||
            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",
 | 
			
		||||
        baseimgtype = {
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_LINT: "docker",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        platform = {
 | 
			
		||||
            ARCH_AMD64: "linux/amd64",
 | 
			
		||||
            ARCH_ARMV7: "linux/arm/v7",
 | 
			
		||||
            ARCH_AARCH64: "linux/arm64",
 | 
			
		||||
        }[arch]
 | 
			
		||||
        target = {
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_LINT: "lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        return cls(
 | 
			
		||||
            build_from=build_from,
 | 
			
		||||
            build_to=build_to,
 | 
			
		||||
            manifest_to=prefix,
 | 
			
		||||
            dockerfile=dockerfile
 | 
			
		||||
            baseimgtype=baseimgtype,
 | 
			
		||||
            platform=platform,
 | 
			
		||||
            target=target,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -112,46 +109,31 @@ def main():
 | 
			
		||||
        # 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_DEV: "cache-dev",
 | 
			
		||||
            CHANNEL_BETA: "cache-beta",
 | 
			
		||||
            CHANNEL_RELEASE: "cache-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
 | 
			
		||||
 | 
			
		||||
        # 3. build
 | 
			
		||||
        cmd = [
 | 
			
		||||
            "docker", "buildx", "build",
 | 
			
		||||
            "--build-arg", f"BASEIMGTYPE={params.baseimgtype}",
 | 
			
		||||
            "--build-arg", f"BUILD_VERSION={args.tag}",
 | 
			
		||||
            "--cache-from", f"type=registry,ref={cache_img}",
 | 
			
		||||
            "--file", "docker/Dockerfile",
 | 
			
		||||
            "--platform", params.platform,
 | 
			
		||||
            "--target", params.target,
 | 
			
		||||
        ]
 | 
			
		||||
        for img in imgs:
 | 
			
		||||
            run_command(
 | 
			
		||||
                "docker", "push", img
 | 
			
		||||
            )
 | 
			
		||||
            cmd += ["--tag", img]
 | 
			
		||||
        if args.push:
 | 
			
		||||
            cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
 | 
			
		||||
 | 
			
		||||
        run_command(*cmd, ".")
 | 
			
		||||
    elif args.command == "manifest":
 | 
			
		||||
        manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								docker/docker_entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										24
									
								
								docker/docker_entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
# If /cache is mounted, use that as PIO's coredir
 | 
			
		||||
# otherwise use path in /config (so that PIO packages aren't downloaded on each compile)
 | 
			
		||||
 | 
			
		||||
if [[ -d /cache ]]; then
 | 
			
		||||
    pio_cache_base=/cache/platformio
 | 
			
		||||
else
 | 
			
		||||
    pio_cache_base=/config/.esphome/platformio
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [[ ! -d "${pio_cache_base}" ]]; then
 | 
			
		||||
    echo "Creating cache directory ${pio_cache_base}"
 | 
			
		||||
    echo "You can change this behavior by mounting a directory to the container's /cache directory."
 | 
			
		||||
    mkdir -p "${pio_cache_base}"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json`
 | 
			
		||||
# setting `core_dir` would therefore prevent pio from accessing
 | 
			
		||||
export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
 | 
			
		||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
 | 
			
		||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
 | 
			
		||||
 | 
			
		||||
exec esphome "$@"
 | 
			
		||||
							
								
								
									
										9
									
								
								docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
#!/usr/bin/with-contenv bashio
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
# Community Hass.io Add-ons: ESPHome
 | 
			
		||||
# This files creates all directories used by esphome
 | 
			
		||||
# ==============================================================================
 | 
			
		||||
 | 
			
		||||
pio_cache_base=/data/cache/platformio
 | 
			
		||||
 | 
			
		||||
mkdir -p "${pio_cache_base}"
 | 
			
		||||
@@ -22,5 +22,14 @@ if bashio::config.has_value 'relative_url'; then
 | 
			
		||||
    export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url')
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
pio_cache_base=/data/cache/platformio
 | 
			
		||||
# we can't set core_dir, because the settings file is stored in `core_dir/appstate.json`
 | 
			
		||||
# setting `core_dir` would therefore prevent pio from accessing
 | 
			
		||||
export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
 | 
			
		||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
 | 
			
		||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
 | 
			
		||||
 | 
			
		||||
export PLATFORMIO_GLOBALLIB_DIR=/piolibs
 | 
			
		||||
 | 
			
		||||
bashio::log.info "Starting ESPHome dashboard..."
 | 
			
		||||
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio
 | 
			
		||||
@@ -3,18 +3,11 @@
 | 
			
		||||
# all platformio libraries in the global storage
 | 
			
		||||
 | 
			
		||||
import configparser
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
config = configparser.ConfigParser()
 | 
			
		||||
config = configparser.ConfigParser(inline_comment_prefixes=(';', ))
 | 
			
		||||
config.read(sys.argv[1])
 | 
			
		||||
libs = []
 | 
			
		||||
for line in config['common']['lib_deps'].splitlines():
 | 
			
		||||
    # Format: '1655@1.0.2  ; TinyGPSPlus (has name conflict)' (includes comment)
 | 
			
		||||
    m = re.search(r'([a-zA-Z0-9-_/]+@[0-9\.]+)', line)
 | 
			
		||||
    if m is None:
 | 
			
		||||
        continue
 | 
			
		||||
    libs.append(m.group(1))
 | 
			
		||||
libs = [x for x in config['common']['lib_deps'].splitlines() if len(x) != 0]
 | 
			
		||||
 | 
			
		||||
subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs])
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
 | 
			
		||||
        if default == "OTA":
 | 
			
		||||
            return CORE.address
 | 
			
		||||
    if show_mqtt and "mqtt" in CORE.config:
 | 
			
		||||
        options.append(("MQTT ({})".format(CORE.config["mqtt"][CONF_BROKER]), "MQTT"))
 | 
			
		||||
        options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
 | 
			
		||||
        if default == "OTA":
 | 
			
		||||
            return "MQTT"
 | 
			
		||||
    if default is not None:
 | 
			
		||||
@@ -180,16 +180,38 @@ def compile_program(args, config):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Compiling app...")
 | 
			
		||||
    return platformio_api.run_compile(config, CORE.verbose)
 | 
			
		||||
    rc = platformio_api.run_compile(config, CORE.verbose)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        return rc
 | 
			
		||||
    idedata = platformio_api.get_idedata(config)
 | 
			
		||||
    return 0 if idedata is not None else 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_using_esptool(config, port):
 | 
			
		||||
    path = CORE.firmware_bin
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
 | 
			
		||||
        "upload_speed", 460800
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def run_esptool(baud_rate):
 | 
			
		||||
        idedata = platformio_api.get_idedata(config)
 | 
			
		||||
 | 
			
		||||
        firmware_offset = "0x10000" if CORE.is_esp32 else "0x0"
 | 
			
		||||
        flash_images = [
 | 
			
		||||
            platformio_api.FlashImage(
 | 
			
		||||
                path=idedata.firmware_bin_path,
 | 
			
		||||
                offset=firmware_offset,
 | 
			
		||||
            ),
 | 
			
		||||
            *idedata.extra_flash_images,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        mcu = "esp8266"
 | 
			
		||||
        if CORE.is_esp32:
 | 
			
		||||
            from esphome.components.esp32 import get_esp32_variant
 | 
			
		||||
 | 
			
		||||
            mcu = get_esp32_variant().lower()
 | 
			
		||||
 | 
			
		||||
        cmd = [
 | 
			
		||||
            "esptool.py",
 | 
			
		||||
            "--before",
 | 
			
		||||
@@ -198,14 +220,15 @@ def upload_using_esptool(config, port):
 | 
			
		||||
            "hard_reset",
 | 
			
		||||
            "--baud",
 | 
			
		||||
            str(baud_rate),
 | 
			
		||||
            "--chip",
 | 
			
		||||
            "esp8266",
 | 
			
		||||
            "--port",
 | 
			
		||||
            port,
 | 
			
		||||
            "--chip",
 | 
			
		||||
            mcu,
 | 
			
		||||
            "write_flash",
 | 
			
		||||
            "0x0",
 | 
			
		||||
            path,
 | 
			
		||||
            "-z",
 | 
			
		||||
        ]
 | 
			
		||||
        for img in flash_images:
 | 
			
		||||
            cmd += [img.offset, img.path]
 | 
			
		||||
 | 
			
		||||
        if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
 | 
			
		||||
            import esptool
 | 
			
		||||
@@ -229,11 +252,7 @@ def upload_using_esptool(config, port):
 | 
			
		||||
def upload_program(config, args, host):
 | 
			
		||||
    # if upload is to a serial port use platformio, otherwise assume ota
 | 
			
		||||
    if get_port_type(host) == "SERIAL":
 | 
			
		||||
        from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
        if CORE.is_esp8266:
 | 
			
		||||
            return upload_using_esptool(config, host)
 | 
			
		||||
        return platformio_api.run_upload(config, CORE.verbose, host)
 | 
			
		||||
        return upload_using_esptool(config, host)
 | 
			
		||||
 | 
			
		||||
    from esphome import espota2
 | 
			
		||||
 | 
			
		||||
@@ -245,7 +264,7 @@ def upload_program(config, args, host):
 | 
			
		||||
 | 
			
		||||
    ota_conf = config[CONF_OTA]
 | 
			
		||||
    remote_port = ota_conf[CONF_PORT]
 | 
			
		||||
    password = ota_conf[CONF_PASSWORD]
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
    return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -256,7 +275,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:
 | 
			
		||||
@@ -415,34 +434,49 @@ def command_update_all(args):
 | 
			
		||||
        click.echo(f"{half_line}{middle_text}{half_line}")
 | 
			
		||||
 | 
			
		||||
    for f in files:
 | 
			
		||||
        print("Updating {}".format(color(Fore.CYAN, f)))
 | 
			
		||||
        print(f"Updating {color(Fore.CYAN, f)}")
 | 
			
		||||
        print("-" * twidth)
 | 
			
		||||
        print()
 | 
			
		||||
        rc = run_external_process(
 | 
			
		||||
            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
        )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f))
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            success[f] = True
 | 
			
		||||
        else:
 | 
			
		||||
            print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f))
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            success[f] = False
 | 
			
		||||
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
 | 
			
		||||
    print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY")))
 | 
			
		||||
    print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
 | 
			
		||||
    failed = 0
 | 
			
		||||
    for f in files:
 | 
			
		||||
        if success[f]:
 | 
			
		||||
            print("  - {}: {}".format(f, color(Fore.GREEN, "SUCCESS")))
 | 
			
		||||
            print(f"  - {f}: {color(Fore.GREEN, 'SUCCESS')}")
 | 
			
		||||
        else:
 | 
			
		||||
            print("  - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED")))
 | 
			
		||||
            print(f"  - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            failed += 1
 | 
			
		||||
    return failed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_idedata(args, config):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
    import json
 | 
			
		||||
 | 
			
		||||
    logging.disable(logging.INFO)
 | 
			
		||||
    logging.disable(logging.WARNING)
 | 
			
		||||
 | 
			
		||||
    idedata = platformio_api.get_idedata(config)
 | 
			
		||||
    if idedata is None:
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    print(json.dumps(idedata.raw, indent=2) + "\n")
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PRE_CONFIG_ACTIONS = {
 | 
			
		||||
    "wizard": command_wizard,
 | 
			
		||||
    "version": command_version,
 | 
			
		||||
@@ -460,6 +494,7 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "clean-mqtt": command_clean_mqtt,
 | 
			
		||||
    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
			
		||||
    "clean": command_clean,
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -483,75 +518,9 @@ def parse_args(argv):
 | 
			
		||||
        metavar=("key", "value"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # 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, construct an ad-hoc parser for the old format 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 continue on with the normal parser (after raising a deprecation warning).
 | 
			
		||||
    #
 | 
			
		||||
    # 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")
 | 
			
		||||
    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",
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # on Python 3.9+ we can simply set exit_on_error=False in the constructor
 | 
			
		||||
    def _raise(x):
 | 
			
		||||
        raise argparse.ArgumentError(None, x)
 | 
			
		||||
 | 
			
		||||
    compat_parser.error = _raise
 | 
			
		||||
 | 
			
		||||
    deprecated_argv_suggestion = None
 | 
			
		||||
 | 
			
		||||
    if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]:
 | 
			
		||||
        # this is most likely meant in new-style arg format. do not try compat parsing
 | 
			
		||||
        pass
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            result, unparsed = compat_parser.parse_known_args(argv[1:])
 | 
			
		||||
            last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
 | 
			
		||||
            unparsed = [
 | 
			
		||||
                "--device" if arg in ("--upload-port", "--serial-port") else arg
 | 
			
		||||
                for arg in unparsed
 | 
			
		||||
            ]
 | 
			
		||||
            argv = (
 | 
			
		||||
                argv[0:last_option] + [result.command] + result.configuration + unparsed
 | 
			
		||||
            )
 | 
			
		||||
            deprecated_argv_suggestion = argv
 | 
			
		||||
        except argparse.ArgumentError:
 | 
			
		||||
            # This is not an old-style command line, so we don't have to do anything.
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    # And continue on with regular parsing
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description=f"ESPHome v{const.__version__}", parents=[options_parser]
 | 
			
		||||
    )
 | 
			
		||||
    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
 | 
			
		||||
 | 
			
		||||
    mqtt_options = argparse.ArgumentParser(add_help=False)
 | 
			
		||||
    mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
 | 
			
		||||
@@ -701,21 +670,107 @@ def parse_args(argv):
 | 
			
		||||
        "configuration", help="Your YAML configuration file directories.", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return parser.parse_args(argv[1:])
 | 
			
		||||
    parser_idedata = subparsers.add_parser("idedata")
 | 
			
		||||
    parser_idedata.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs=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):
 | 
			
		||||
    args = parse_args(argv)
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
 | 
			
		||||
    setup_log(args.verbose, args.quiet)
 | 
			
		||||
    setup_log(
 | 
			
		||||
        args.verbose,
 | 
			
		||||
        args.quiet,
 | 
			
		||||
        # Show timestamp for dashboard access logs
 | 
			
		||||
        args.command == "dashboard",
 | 
			
		||||
    )
 | 
			
		||||
    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[1:]))
 | 
			
		||||
        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion))
 | 
			
		||||
 | 
			
		||||
    if sys.version_info < (3, 7, 0):
 | 
			
		||||
        _LOGGER.error(
 | 
			
		||||
@@ -737,7 +792,7 @@ def run_esphome(argv):
 | 
			
		||||
 | 
			
		||||
        config = read_config(dict(args.substitution) if args.substitution else {})
 | 
			
		||||
        if config is None:
 | 
			
		||||
            return 1
 | 
			
		||||
            return 2
 | 
			
		||||
        CORE.config = config
 | 
			
		||||
 | 
			
		||||
        if args.command not in POST_CONFIG_ACTIONS:
 | 
			
		||||
 
 | 
			
		||||
										
											
												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
 | 
			
		||||
@@ -6,6 +6,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ELSE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_THEN,
 | 
			
		||||
    CONF_TIMEOUT,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_TYPE_ID,
 | 
			
		||||
    CONF_TIME,
 | 
			
		||||
@@ -244,6 +245,9 @@ def validate_wait_until(value):
 | 
			
		||||
    schema = cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_CONDITION): validate_potentially_and_condition,
 | 
			
		||||
            cv.Optional(CONF_TIMEOUT): cv.templatable(
 | 
			
		||||
                cv.positive_time_period_milliseconds
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    if isinstance(value, dict) and CONF_CONDITION in value:
 | 
			
		||||
@@ -255,6 +259,9 @@ def validate_wait_until(value):
 | 
			
		||||
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)
 | 
			
		||||
    if CONF_TIMEOUT in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
 | 
			
		||||
        cg.add(var.set_timeout_value(template_))
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ from esphome.cpp_generator import (  # noqa
 | 
			
		||||
    add_library,
 | 
			
		||||
    add_build_flag,
 | 
			
		||||
    add_define,
 | 
			
		||||
    add_platformio_option,
 | 
			
		||||
    get_variable,
 | 
			
		||||
    get_variable_with_full_id,
 | 
			
		||||
    process_lambda,
 | 
			
		||||
@@ -66,7 +67,7 @@ from esphome.cpp_types import (  # noqa
 | 
			
		||||
    NAN,
 | 
			
		||||
    esphome_ns,
 | 
			
		||||
    App,
 | 
			
		||||
    Nameable,
 | 
			
		||||
    EntityBase,
 | 
			
		||||
    Component,
 | 
			
		||||
    ComponentPtr,
 | 
			
		||||
    PollingComponent,
 | 
			
		||||
@@ -78,4 +79,6 @@ from esphome.cpp_types import (  # noqa
 | 
			
		||||
    JsonObjectConstRef,
 | 
			
		||||
    Controller,
 | 
			
		||||
    GPIOPin,
 | 
			
		||||
    InternalGPIOPin,
 | 
			
		||||
    gpio_Flags,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/esphal.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/stepper/stepper.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,16 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "ac_dimmer.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <cmath>
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#include <core_esp8266_waveform.h>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
 | 
			
		||||
#include <esp32-hal-timer.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ac_dimmer {
 | 
			
		||||
@@ -17,12 +23,15 @@ static AcDimmerDataStore *all_dimmers[32];  // NOLINT(cppcoreguidelines-avoid-no
 | 
			
		||||
/// 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 const uint32_t GATE_ENABLE_TIME = 10;
 | 
			
		||||
/// However other factors like gate driver propagation time
 | 
			
		||||
/// are also considered and a really low value is not important
 | 
			
		||||
/// See also: https://github.com/esphome/issues/issues/1632
 | 
			
		||||
static const uint32_t GATE_ENABLE_TIME = 50;
 | 
			
		||||
 | 
			
		||||
/// Function called from timer interrupt
 | 
			
		||||
/// Input is current time in microseconds (micros())
 | 
			
		||||
/// Returns when next "event" is expected in µs, or 0 if no such event known.
 | 
			
		||||
uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
 | 
			
		||||
uint32_t IRAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
 | 
			
		||||
  // If no ZC signal received yet.
 | 
			
		||||
  if (this->crossed_zero_at == 0)
 | 
			
		||||
    return 0;
 | 
			
		||||
@@ -34,13 +43,13 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
 | 
			
		||||
 | 
			
		||||
  if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) {
 | 
			
		||||
    this->enable_time_us = 0;
 | 
			
		||||
    this->gate_pin->digital_write(true);
 | 
			
		||||
    this->gate_pin.digital_write(true);
 | 
			
		||||
    // Prevent too short pulses
 | 
			
		||||
    this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME);
 | 
			
		||||
    this->disable_time_us = std::max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) {
 | 
			
		||||
    this->disable_time_us = 0;
 | 
			
		||||
    this->gate_pin->digital_write(false);
 | 
			
		||||
    this->gate_pin.digital_write(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (time_since_zc < this->enable_time_us)
 | 
			
		||||
@@ -60,7 +69,7 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Run timer interrupt code and return in how many µs the next event is expected
 | 
			
		||||
uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() {
 | 
			
		||||
uint32_t IRAM_ATTR HOT timer_interrupt() {
 | 
			
		||||
  // run at least with 1kHz
 | 
			
		||||
  uint32_t min_dt_us = 1000;
 | 
			
		||||
  uint32_t now = micros();
 | 
			
		||||
@@ -77,7 +86,7 @@ uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// GPIO interrupt routine, called when ZC pin triggers
 | 
			
		||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
  uint32_t prev_crossed = this->crossed_zero_at;
 | 
			
		||||
 | 
			
		||||
  // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms
 | 
			
		||||
@@ -94,7 +103,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
 | 
			
		||||
  if (this->value == 65535) {
 | 
			
		||||
    // fully on, enable output immediately
 | 
			
		||||
    this->gate_pin->digital_write(true);
 | 
			
		||||
    this->gate_pin.digital_write(true);
 | 
			
		||||
  } else if (this->init_cycle) {
 | 
			
		||||
    // send a full cycle
 | 
			
		||||
    this->init_cycle = false;
 | 
			
		||||
@@ -102,29 +111,29 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
    this->disable_time_us = cycle_time_us;
 | 
			
		||||
  } else if (this->value == 0) {
 | 
			
		||||
    // fully off, disable output immediately
 | 
			
		||||
    this->gate_pin->digital_write(false);
 | 
			
		||||
    this->gate_pin.digital_write(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->method == DIM_METHOD_TRAILING) {
 | 
			
		||||
      this->enable_time_us = 1;  // cannot be 0
 | 
			
		||||
      this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
 | 
			
		||||
      this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
 | 
			
		||||
    } else {
 | 
			
		||||
      // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
 | 
			
		||||
      // also take into account min_power
 | 
			
		||||
      auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
      this->enable_time_us = max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
      this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
      if (this->method == DIM_METHOD_LEADING_PULSE) {
 | 
			
		||||
        // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
 | 
			
		||||
        // this is for brightness near 99%
 | 
			
		||||
        this->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10);
 | 
			
		||||
        this->disable_time_us = std::max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10);
 | 
			
		||||
      } else {
 | 
			
		||||
        this->gate_pin->digital_write(false);
 | 
			
		||||
        this->gate_pin.digital_write(false);
 | 
			
		||||
        this->disable_time_us = this->cycle_time_us;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
 | 
			
		||||
void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
 | 
			
		||||
  // 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
 | 
			
		||||
@@ -138,11 +147,11 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
// ESP32 implementation, uses basically the same code but needs to wrap
 | 
			
		||||
// timer_interrupt() function to auto-reschedule
 | 
			
		||||
static hw_timer_t *dimmer_timer = nullptr;
 | 
			
		||||
void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
 | 
			
		||||
static hw_timer_t *dimmer_timer = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void AcDimmer::setup() {
 | 
			
		||||
@@ -171,15 +180,16 @@ void AcDimmer::setup() {
 | 
			
		||||
  if (setup_zero_cross_pin) {
 | 
			
		||||
    this->zero_cross_pin_->setup();
 | 
			
		||||
    this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
 | 
			
		||||
    this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING);
 | 
			
		||||
    this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_,
 | 
			
		||||
                                            gpio::INTERRUPT_FALLING_EDGE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
  // Uses ESP8266 waveform (soft PWM) class
 | 
			
		||||
  // PWM and AcDimmer can even run at the same time this way
 | 
			
		||||
  setTimer1Callback(&timer_interrupt);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  // 80 Divider -> 1 count=1µs
 | 
			
		||||
  dimmer_timer = timerBegin(0, 80, true);
 | 
			
		||||
  timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true);
 | 
			
		||||
@@ -215,3 +225,5 @@ void AcDimmer::dump_config() {
 | 
			
		||||
 | 
			
		||||
}  // namespace ac_dimmer
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ARDUINO
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/esphal.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/output/float_output.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -11,11 +13,11 @@ enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TR
 | 
			
		||||
 | 
			
		||||
struct AcDimmerDataStore {
 | 
			
		||||
  /// Zero-cross pin
 | 
			
		||||
  ISRInternalGPIOPin *zero_cross_pin;
 | 
			
		||||
  ISRInternalGPIOPin zero_cross_pin;
 | 
			
		||||
  /// Zero-cross pin number - used to share ZC pin across multiple dimmers
 | 
			
		||||
  uint8_t zero_cross_pin_number;
 | 
			
		||||
  /// Output pin to write to
 | 
			
		||||
  ISRInternalGPIOPin *gate_pin;
 | 
			
		||||
  ISRInternalGPIOPin gate_pin;
 | 
			
		||||
  /// Value of the dimmer - 0 to 65535.
 | 
			
		||||
  uint16_t value;
 | 
			
		||||
  /// Minimum power for activation
 | 
			
		||||
@@ -37,7 +39,7 @@ struct AcDimmerDataStore {
 | 
			
		||||
 | 
			
		||||
  void gpio_intr();
 | 
			
		||||
  static void s_gpio_intr(AcDimmerDataStore *store);
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  static void s_timer_intr();
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
@@ -47,16 +49,16 @@ class AcDimmer : public output::FloatOutput, public Component {
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; }
 | 
			
		||||
  void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
 | 
			
		||||
  void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; }
 | 
			
		||||
  void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
 | 
			
		||||
  void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
 | 
			
		||||
  void set_method(DimMethod method) { method_ = method; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void write_state(float state) override;
 | 
			
		||||
 | 
			
		||||
  GPIOPin *gate_pin_;
 | 
			
		||||
  GPIOPin *zero_cross_pin_;
 | 
			
		||||
  InternalGPIOPin *gate_pin_;
 | 
			
		||||
  InternalGPIOPin *zero_cross_pin_;
 | 
			
		||||
  AcDimmerDataStore store_;
 | 
			
		||||
  bool init_with_half_cycle_;
 | 
			
		||||
  DimMethod method_;
 | 
			
		||||
@@ -64,3 +66,5 @@ class AcDimmer : public output::FloatOutput, public Component {
 | 
			
		||||
 | 
			
		||||
}  // namespace ac_dimmer
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ARDUINO
 | 
			
		||||
 
 | 
			
		||||
@@ -19,17 +19,20 @@ DIM_METHODS = {
 | 
			
		||||
CONF_GATE_PIN = "gate_pin"
 | 
			
		||||
CONF_ZERO_CROSS_PIN = "zero_cross_pin"
 | 
			
		||||
CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle"
 | 
			
		||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.declare_id(AcDimmer),
 | 
			
		||||
        cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
 | 
			
		||||
        cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
 | 
			
		||||
        cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
 | 
			
		||||
        cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum(
 | 
			
		||||
            DIM_METHODS, upper=True, space="_"
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    output.FLOAT_OUTPUT_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.declare_id(AcDimmer),
 | 
			
		||||
            cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
 | 
			
		||||
            cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum(
 | 
			
		||||
                DIM_METHODS, upper=True, space="_"
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_arduino,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
 | 
			
		||||
  for (int led = it.size(); led-- > 0;) {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,13 @@
 | 
			
		||||
#include "adc_sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#ifdef USE_ADC_SENSOR_VCC
 | 
			
		||||
#include <Esp.h>
 | 
			
		||||
ADC_MODE(ADC_VCC)
 | 
			
		||||
#else
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -10,7 +15,7 @@ namespace adc {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "adc";
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
 | 
			
		||||
 | 
			
		||||
inline adc1_channel_t gpio_to_adc1(uint8_t pin) {
 | 
			
		||||
@@ -57,28 +62,28 @@ inline adc1_channel_t gpio_to_adc1(uint8_t pin) {
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
 | 
			
		||||
#ifndef USE_ADC_SENSOR_VCC
 | 
			
		||||
  GPIOPin(this->pin_, INPUT).setup();
 | 
			
		||||
  pin_->setup();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  adc1_config_channel_atten(gpio_to_adc1(pin_), attenuation_);
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  adc1_config_channel_atten(gpio_to_adc1(pin_->get_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_));
 | 
			
		||||
  adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin()));
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void ADCSensor::dump_config() {
 | 
			
		||||
  LOG_SENSOR("", "ADC Sensor", this);
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#ifdef USE_ADC_SENSOR_VCC
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Pin: VCC");
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_);
 | 
			
		||||
  LOG_PIN("  Pin: ", pin_);
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_);
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  LOG_PIN("  Pin: ", pin_);
 | 
			
		||||
  switch (this->attenuation_) {
 | 
			
		||||
    case ADC_ATTEN_DB_0:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
 | 
			
		||||
@@ -105,8 +110,8 @@ void ADCSensor::update() {
 | 
			
		||||
  this->publish_state(value_v);
 | 
			
		||||
}
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
  int raw = adc1_get_raw(gpio_to_adc1(pin_));
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin()));
 | 
			
		||||
  float value_v = raw / 4095.0f;
 | 
			
		||||
#if CONFIG_IDF_TARGET_ESP32
 | 
			
		||||
  switch (this->attenuation_) {
 | 
			
		||||
@@ -146,15 +151,15 @@ float ADCSensor::sample() {
 | 
			
		||||
  return value_v;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#ifdef USE_ADC_SENSOR_VCC
 | 
			
		||||
  return ESP.getVcc() / 1024.0f;
 | 
			
		||||
  return ESP.getVcc() / 1024.0f;  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
#else
 | 
			
		||||
  return analogRead(this->pin_) / 1024.0f;  // NOLINT
 | 
			
		||||
  return analogRead(this->pin_->get_pin()) / 1024.0f;  // NOLINT
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/esphal.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/voltage_sampler/voltage_sampler.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include "driver/adc.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace adc {
 | 
			
		||||
 | 
			
		||||
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
 | 
			
		||||
 public:
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  /// Set the attenuation for this pin. Only available on the ESP32.
 | 
			
		||||
  void set_attenuation(adc_atten_t attenuation);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -27,17 +27,17 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// `HARDWARE_LATE` setup priority.
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void set_pin(uint8_t pin) { this->pin_ = pin; }
 | 
			
		||||
  void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
 | 
			
		||||
  float sample() override;
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
  std::string unique_id() override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint8_t pin_;
 | 
			
		||||
  InternalGPIOPin *pin_;
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  adc_atten_t attenuation_{ADC_ATTEN_DB_0};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,13 @@ from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ATTENUATION,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INPUT,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
@@ -23,10 +25,34 @@ ATTENUATION_MODES = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_adc_pin(value):
 | 
			
		||||
    vcc = str(value).upper()
 | 
			
		||||
    if vcc == "VCC":
 | 
			
		||||
        return cv.only_on_esp8266(vcc)
 | 
			
		||||
    return pins.analog_pin(value)
 | 
			
		||||
    if str(value).upper() == "VCC":
 | 
			
		||||
        return cv.only_on_esp8266("VCC")
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        from esphome.components.esp32 import is_esp32c3
 | 
			
		||||
 | 
			
		||||
        value = pins.internal_gpio_input_pin_number(value)
 | 
			
		||||
        if is_esp32c3():
 | 
			
		||||
            if not (0 <= value <= 4):  # ADC1
 | 
			
		||||
                raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.")
 | 
			
		||||
        elif not (32 <= value <= 39):  # ADC1
 | 
			
		||||
            raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.")
 | 
			
		||||
    elif CORE.is_esp8266:
 | 
			
		||||
        from esphome.components.esp8266.gpio import CONF_ANALOG
 | 
			
		||||
 | 
			
		||||
        value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
 | 
			
		||||
            value
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if value != 17:  # A0
 | 
			
		||||
            raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.")
 | 
			
		||||
        return pins.gpio_pin_schema(
 | 
			
		||||
            {CONF_ANALOG: True, CONF_INPUT: True}, internal=True
 | 
			
		||||
        )(value)
 | 
			
		||||
    else:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    return pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
adc_ns = cg.esphome_ns.namespace("adc")
 | 
			
		||||
@@ -62,7 +88,8 @@ async def to_code(config):
 | 
			
		||||
    if config[CONF_PIN] == "VCC":
 | 
			
		||||
        cg.add_define("USE_ADC_SENSOR_VCC")
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_pin(config[CONF_PIN]))
 | 
			
		||||
        pin = await cg.gpio_pin_expression(config[CONF_PIN])
 | 
			
		||||
        cg.add(var.set_pin(pin))
 | 
			
		||||
 | 
			
		||||
    if CONF_ATTENUATION in config:
 | 
			
		||||
        cg.add(var.set_attenuation(config[CONF_ATTENUATION]))
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,7 @@ static const char *const TAG = "ade7953";
 | 
			
		||||
 | 
			
		||||
void ADE7953::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ADE7953:");
 | 
			
		||||
  if (this->has_irq_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  IRQ Pin: GPIO%u", this->irq_pin_number_);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_PIN("  IRQ Pin: ", irq_pin_);
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Voltage Sensor", this->voltage_sensor_);
 | 
			
		||||
@@ -20,27 +18,28 @@ void ADE7953::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Active Power B Sensor", this->active_power_b_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define ADE_PUBLISH_(name, factor) \
 | 
			
		||||
  if ((name) && this->name##_sensor_) { \
 | 
			
		||||
    float value = *(name) / (factor); \
 | 
			
		||||
#define ADE_PUBLISH_(name, val, factor) \
 | 
			
		||||
  if (err == i2c::ERROR_OK && this->name##_sensor_) { \
 | 
			
		||||
    float value = (val) / (factor); \
 | 
			
		||||
    this->name##_sensor_->publish_state(value); \
 | 
			
		||||
  }
 | 
			
		||||
#define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor)
 | 
			
		||||
#define ADE_PUBLISH(name, val, factor) ADE_PUBLISH_(name, val, factor)
 | 
			
		||||
 | 
			
		||||
void ADE7953::update() {
 | 
			
		||||
  if (!this->is_setup_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto active_power_a = this->ade_read_<int32_t>(0x0312);
 | 
			
		||||
  ADE_PUBLISH(active_power_a, 154.0f);
 | 
			
		||||
  auto active_power_b = this->ade_read_<int32_t>(0x0313);
 | 
			
		||||
  ADE_PUBLISH(active_power_b, 154.0f);
 | 
			
		||||
  auto current_a = this->ade_read_<uint32_t>(0x031A);
 | 
			
		||||
  ADE_PUBLISH(current_a, 100000.0f);
 | 
			
		||||
  auto current_b = this->ade_read_<uint32_t>(0x031B);
 | 
			
		||||
  ADE_PUBLISH(current_b, 100000.0f);
 | 
			
		||||
  auto voltage = this->ade_read_<uint32_t>(0x031C);
 | 
			
		||||
  ADE_PUBLISH(voltage, 26000.0f);
 | 
			
		||||
  uint32_t val;
 | 
			
		||||
  i2c::ErrorCode err = ade_read_32_(0x0312, &val);
 | 
			
		||||
  ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f);
 | 
			
		||||
  err = ade_read_32_(0x0313, &val);
 | 
			
		||||
  ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f);
 | 
			
		||||
  err = ade_read_32_(0x031A, &val);
 | 
			
		||||
  ADE_PUBLISH(current_a, (uint32_t) val, 100000.0f);
 | 
			
		||||
  err = ade_read_32_(0x031B, &val);
 | 
			
		||||
  ADE_PUBLISH(current_b, (uint32_t) val, 100000.0f);
 | 
			
		||||
  err = ade_read_32_(0x031C, &val);
 | 
			
		||||
  ADE_PUBLISH(voltage, (uint32_t) val, 26000.0f);
 | 
			
		||||
 | 
			
		||||
  //    auto apparent_power_a = this->ade_read_<int32_t>(0x0310);
 | 
			
		||||
  //    auto apparent_power_b = this->ade_read_<int32_t>(0x0311);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
@@ -9,10 +10,7 @@ namespace ade7953 {
 | 
			
		||||
 | 
			
		||||
class ADE7953 : public i2c::I2CDevice, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_irq_pin(uint8_t irq_pin) {
 | 
			
		||||
    has_irq_ = true;
 | 
			
		||||
    irq_pin_number_ = irq_pin;
 | 
			
		||||
  }
 | 
			
		||||
  void set_irq_pin(InternalGPIOPin *irq_pin) { irq_pin_ = irq_pin; }
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
 | 
			
		||||
  void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; }
 | 
			
		||||
  void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; }
 | 
			
		||||
@@ -24,15 +22,13 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setup() override {
 | 
			
		||||
    if (this->has_irq_) {
 | 
			
		||||
      auto pin = GPIOPin(this->irq_pin_number_, INPUT);
 | 
			
		||||
      this->irq_pin_ = &pin;
 | 
			
		||||
    if (this->irq_pin_ != nullptr) {
 | 
			
		||||
      this->irq_pin_->setup();
 | 
			
		||||
    }
 | 
			
		||||
    this->set_timeout(100, [this]() {
 | 
			
		||||
      this->ade_write_<uint8_t>(0x0010, 0x04);
 | 
			
		||||
      this->ade_write_<uint8_t>(0x00FE, 0xAD);
 | 
			
		||||
      this->ade_write_<uint16_t>(0x0120, 0x0030);
 | 
			
		||||
      this->ade_write_8_(0x0010, 0x04);
 | 
			
		||||
      this->ade_write_8_(0x00FE, 0xAD);
 | 
			
		||||
      this->ade_write_16_(0x0120, 0x0030);
 | 
			
		||||
      this->is_setup_ = true;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@@ -42,31 +38,51 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent {
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  template<typename T> bool ade_write_(uint16_t reg, T value) {
 | 
			
		||||
  i2c::ErrorCode ade_write_8_(uint16_t reg, uint8_t value) {
 | 
			
		||||
    std::vector<uint8_t> data;
 | 
			
		||||
    data.push_back(reg >> 8);
 | 
			
		||||
    data.push_back(reg >> 0);
 | 
			
		||||
    for (int i = sizeof(T) - 1; i >= 0; i--)
 | 
			
		||||
      data.push_back(value >> (i * 8));
 | 
			
		||||
    return this->write_bytes_raw(data);
 | 
			
		||||
    data.push_back(value);
 | 
			
		||||
    return write(data.data(), data.size());
 | 
			
		||||
  }
 | 
			
		||||
  template<typename T> optional<T> ade_read_(uint16_t reg) {
 | 
			
		||||
    uint8_t hi = reg >> 8;
 | 
			
		||||
    uint8_t lo = reg >> 0;
 | 
			
		||||
    if (!this->write_bytes_raw({hi, lo}))
 | 
			
		||||
      return {};
 | 
			
		||||
    auto ret = this->read_bytes_raw<sizeof(T)>();
 | 
			
		||||
    if (!ret.has_value())
 | 
			
		||||
      return {};
 | 
			
		||||
    T result = 0;
 | 
			
		||||
    for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--)
 | 
			
		||||
      result |= T((*ret)[i]) << (j * 8);
 | 
			
		||||
    return result;
 | 
			
		||||
  i2c::ErrorCode ade_write_16_(uint16_t reg, uint16_t value) {
 | 
			
		||||
    std::vector<uint8_t> data;
 | 
			
		||||
    data.push_back(reg >> 8);
 | 
			
		||||
    data.push_back(reg >> 0);
 | 
			
		||||
    data.push_back(value >> 8);
 | 
			
		||||
    data.push_back(value >> 0);
 | 
			
		||||
    return write(data.data(), data.size());
 | 
			
		||||
  }
 | 
			
		||||
  i2c::ErrorCode ade_write_32_(uint16_t reg, uint32_t value) {
 | 
			
		||||
    std::vector<uint8_t> data;
 | 
			
		||||
    data.push_back(reg >> 8);
 | 
			
		||||
    data.push_back(reg >> 0);
 | 
			
		||||
    data.push_back(value >> 24);
 | 
			
		||||
    data.push_back(value >> 16);
 | 
			
		||||
    data.push_back(value >> 8);
 | 
			
		||||
    data.push_back(value >> 0);
 | 
			
		||||
    return write(data.data(), data.size());
 | 
			
		||||
  }
 | 
			
		||||
  i2c::ErrorCode ade_read_32_(uint16_t reg, uint32_t *value) {
 | 
			
		||||
    uint8_t reg_data[2];
 | 
			
		||||
    reg_data[0] = reg >> 8;
 | 
			
		||||
    reg_data[1] = reg >> 0;
 | 
			
		||||
    i2c::ErrorCode err = write(reg_data, 2);
 | 
			
		||||
    if (err != i2c::ERROR_OK)
 | 
			
		||||
      return err;
 | 
			
		||||
    uint8_t recv[4];
 | 
			
		||||
    err = read(recv, 4);
 | 
			
		||||
    if (err != i2c::ERROR_OK)
 | 
			
		||||
      return err;
 | 
			
		||||
    *value = 0;
 | 
			
		||||
    *value |= ((uint32_t) recv[0]) << 24;
 | 
			
		||||
    *value |= ((uint32_t) recv[1]) << 16;
 | 
			
		||||
    *value |= ((uint32_t) recv[2]) << 8;
 | 
			
		||||
    *value |= ((uint32_t) recv[3]);
 | 
			
		||||
    return i2c::ERROR_OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool has_irq_ = false;
 | 
			
		||||
  uint8_t irq_pin_number_;
 | 
			
		||||
  GPIOPin *irq_pin_{nullptr};
 | 
			
		||||
  InternalGPIOPin *irq_pin_ = nullptr;
 | 
			
		||||
  bool is_setup_{false};
 | 
			
		||||
  sensor::Sensor *voltage_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *current_a_sensor_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADE7953),
 | 
			
		||||
            cv.Optional(CONF_IRQ_PIN): pins.input_pin,
 | 
			
		||||
            cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
@@ -73,7 +73,8 @@ async def to_code(config):
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_IRQ_PIN in config:
 | 
			
		||||
        cg.add(var.set_irq_pin(config[CONF_IRQ_PIN]))
 | 
			
		||||
        irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
 | 
			
		||||
        cg.add(var.set_irq_pin(irq_pin))
 | 
			
		||||
 | 
			
		||||
    for key in [
 | 
			
		||||
        CONF_VOLTAGE,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "ads1115.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ads1115 {
 | 
			
		||||
@@ -159,7 +160,7 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
 | 
			
		||||
float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); }
 | 
			
		||||
void ADS1115Sensor::update() {
 | 
			
		||||
  float v = this->parent_->request_measurement(this);
 | 
			
		||||
  if (!isnan(v)) {
 | 
			
		||||
  if (!std::isnan(v)) {
 | 
			
		||||
    ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v);
 | 
			
		||||
    this->publish_state(v);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
#include "aht10.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace aht10 {
 | 
			
		||||
@@ -33,8 +34,19 @@ void AHT10Component::setup() {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t data;
 | 
			
		||||
  if (!this->read_byte(0, &data, AHT10_DEFAULT_DELAY)) {
 | 
			
		||||
  uint8_t data = 0;
 | 
			
		||||
  if (this->write(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  delay(AHT10_DEFAULT_DELAY);
 | 
			
		||||
  if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -55,15 +67,26 @@ void AHT10Component::update() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t data[6];
 | 
			
		||||
  uint8_t delay = AHT10_DEFAULT_DELAY;
 | 
			
		||||
  uint8_t delay_ms = AHT10_DEFAULT_DELAY;
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    delay = AHT10_HUMIDITY_DELAY;
 | 
			
		||||
    delay_ms = AHT10_HUMIDITY_DELAY;
 | 
			
		||||
  bool success = false;
 | 
			
		||||
  for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Attempt %u at %6ld", i, millis());
 | 
			
		||||
    ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis());
 | 
			
		||||
    delay_microseconds_accurate(4);
 | 
			
		||||
    if (!this->read_bytes(0, data, 6, delay)) {
 | 
			
		||||
 | 
			
		||||
    uint8_t reg = 0;
 | 
			
		||||
    if (this->write(®, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
 | 
			
		||||
    } else if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    delay(delay_ms);
 | 
			
		||||
    if (this->read(data, 6) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy
 | 
			
		||||
      ESP_LOGD(TAG, "AHT10 is busy, waiting...");
 | 
			
		||||
    } else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
 | 
			
		||||
      // Unrealistic humidity (0x0)
 | 
			
		||||
@@ -80,11 +103,12 @@ void AHT10Component::update() {
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // data is valid, we can break the loop
 | 
			
		||||
      ESP_LOGVV(TAG, "Answer at %6ld", millis());
 | 
			
		||||
      ESP_LOGVV(TAG, "Answer at %6u", millis());
 | 
			
		||||
      success = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if ((data[0] & 0x80) == 0x80) {
 | 
			
		||||
  if (!success || (data[0] & 0x80) == 0x80) {
 | 
			
		||||
    ESP_LOGE(TAG, "Measurements reading timed-out!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -105,7 +129,7 @@ void AHT10Component::update() {
 | 
			
		||||
    this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    if (isnan(humidity))
 | 
			
		||||
    if (std::isnan(humidity))
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum");
 | 
			
		||||
    this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_ble {
 | 
			
		||||
 | 
			
		||||
static const char *const 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
 | 
			
		||||
							
								
								
									
										19
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.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_mini/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_mini/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@ncareau"]
 | 
			
		||||
							
								
								
									
										113
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
#include "airthings_wave_mini.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_mini {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "airthings_wave_mini";
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveMini::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 = esp32_ble_tracker::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 AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
  auto value = (WaveMiniReadings *) raw_value;
 | 
			
		||||
 | 
			
		||||
  if (sizeof(WaveMiniReadings) <= value_len) {
 | 
			
		||||
    this->humidity_sensor_->publish_state(value->humidity / 100.0f);
 | 
			
		||||
    this->pressure_sensor_->publish_state(value->pressure / 50.0f);
 | 
			
		||||
    this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveMini::update() {
 | 
			
		||||
  if (this->node_state != esp32_ble_tracker::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 AirthingsWaveMini::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 AirthingsWaveMini::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirthingsWaveMini::AirthingsWaveMini()
 | 
			
		||||
    : PollingComponent(10000),
 | 
			
		||||
      service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
 | 
			
		||||
      sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_mini
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
							
								
								
									
										65
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#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/component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_mini {
 | 
			
		||||
 | 
			
		||||
static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
 | 
			
		||||
class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  AirthingsWaveMini();
 | 
			
		||||
 | 
			
		||||
  void dump_config() 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 set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
 | 
			
		||||
  void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_valid_voc_value_(uint16_t voc);
 | 
			
		||||
 | 
			
		||||
  void read_sensors_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  void request_read_values_();
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *pressure_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *tvoc_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  uint16_t handle_;
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID service_uuid_;
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
 | 
			
		||||
 | 
			
		||||
  struct WaveMiniReadings {
 | 
			
		||||
    uint16_t unused01;
 | 
			
		||||
    uint16_t temperature;
 | 
			
		||||
    uint16_t pressure;
 | 
			
		||||
    uint16_t humidity;
 | 
			
		||||
    uint16_t voc;
 | 
			
		||||
    uint16_t unused02;
 | 
			
		||||
    uint32_t unused03;
 | 
			
		||||
    uint32_t unused04;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_mini
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
							
								
								
									
										82
									
								
								esphome/components/airthings_wave_mini/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								esphome/components/airthings_wave_mini/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini")
 | 
			
		||||
AirthingsWaveMini = airthings_wave_mini_ns.class_(
 | 
			
		||||
    "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            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=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
                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("5min"))
 | 
			
		||||
    .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_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_TVOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
        cg.add(var.set_tvoc(sens))
 | 
			
		||||
							
								
								
									
										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"]
 | 
			
		||||
							
								
								
									
										137
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
#include "airthings_wave_plus.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "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 = esp32_ble_tracker::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_(uint16_t radon) { return 0 <= radon && radon <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::update() {
 | 
			
		||||
  if (this->node_state != esp32_ble_tracker::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),
 | 
			
		||||
      service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
 | 
			
		||||
      sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
							
								
								
									
										75
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#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/component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
 | 
			
		||||
class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  AirthingsWavePlus();
 | 
			
		||||
 | 
			
		||||
  void dump_config() 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 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_(uint16_t radon);
 | 
			
		||||
  bool is_valid_voc_value_(uint16_t voc);
 | 
			
		||||
  bool is_valid_co2_value_(uint16_t 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_;
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID service_uuid_;
 | 
			
		||||
  esp32_ble_tracker::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  // USE_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.All(
 | 
			
		||||
    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("5min"))
 | 
			
		||||
    .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))
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
#include "am2320.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am2320 {
 | 
			
		||||
@@ -77,7 +78,7 @@ bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len
 | 
			
		||||
 | 
			
		||||
  if (conversion > 0)
 | 
			
		||||
    delay(conversion);
 | 
			
		||||
  return this->parent_->raw_receive(this->address_, data, len);
 | 
			
		||||
  return this->read(data, len) == i2c::ERROR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AM2320Component::read_data_(uint8_t *data) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
#include "am43.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "am43";
 | 
			
		||||
static const char *const TAG = "am43";
 | 
			
		||||
 | 
			
		||||
void Am43::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "AM43");
 | 
			
		||||
@@ -15,8 +16,8 @@ void Am43::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43::setup() {
 | 
			
		||||
  this->encoder_ = new Am43Encoder();
 | 
			
		||||
  this->decoder_ = new Am43Decoder();
 | 
			
		||||
  this->encoder_ = make_unique<Am43Encoder>();
 | 
			
		||||
  this->decoder_ = make_unique<Am43Decoder>();
 | 
			
		||||
  this->logged_in_ = false;
 | 
			
		||||
  this->last_battery_update_ = 0;
 | 
			
		||||
  this->current_sensor_ = 0;
 | 
			
		||||
@@ -30,7 +31,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->logged_in_ = false;
 | 
			
		||||
      this->node_state = espbt::ClientState::Idle;
 | 
			
		||||
      this->node_state = espbt::ClientState::IDLE;
 | 
			
		||||
      if (this->battery_ != nullptr)
 | 
			
		||||
        this->battery_->publish_state(NAN);
 | 
			
		||||
      if (this->illuminance_ != nullptr)
 | 
			
		||||
@@ -53,7 +54,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      this->update();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -92,7 +93,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/am43/am43_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
@@ -28,8 +28,8 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  Am43Encoder *encoder_;
 | 
			
		||||
  Am43Decoder *decoder_;
 | 
			
		||||
  std::unique_ptr<Am43Encoder> encoder_;
 | 
			
		||||
  std::unique_ptr<Am43Decoder> decoder_;
 | 
			
		||||
  bool logged_in_;
 | 
			
		||||
  sensor::Sensor *battery_{nullptr};
 | 
			
		||||
  sensor::Sensor *illuminance_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
#include "am43_base.h"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
#include "am43_cover.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am43 {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "am43_cover";
 | 
			
		||||
static const char *const TAG = "am43_cover";
 | 
			
		||||
 | 
			
		||||
using namespace esphome::cover;
 | 
			
		||||
 | 
			
		||||
@@ -18,13 +18,13 @@ void Am43Component::dump_config() {
 | 
			
		||||
 | 
			
		||||
void Am43Component::setup() {
 | 
			
		||||
  this->position = COVER_OPEN;
 | 
			
		||||
  this->encoder_ = new Am43Encoder();
 | 
			
		||||
  this->decoder_ = new Am43Decoder();
 | 
			
		||||
  this->encoder_ = make_unique<Am43Encoder>();
 | 
			
		||||
  this->decoder_ = make_unique<Am43Decoder>();
 | 
			
		||||
  this->logged_in_ = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::loop() {
 | 
			
		||||
  if (this->node_state == espbt::ClientState::Established && !this->logged_in_) {
 | 
			
		||||
  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,
 | 
			
		||||
@@ -46,7 +46,7 @@ CoverTraits Am43Component::get_traits() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Am43Component::control(const CoverCall &call) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -98,7 +98,7 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
#include "esphome/components/cover/cover.h"
 | 
			
		||||
#include "esphome/components/am43/am43_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
@@ -32,8 +32,8 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  uint16_t pin_;
 | 
			
		||||
  bool invert_position_;
 | 
			
		||||
  Am43Encoder *encoder_;
 | 
			
		||||
  Am43Decoder *decoder_;
 | 
			
		||||
  std::unique_ptr<Am43Encoder> encoder_;
 | 
			
		||||
  std::unique_ptr<Am43Decoder> decoder_;
 | 
			
		||||
  bool logged_in_;
 | 
			
		||||
 | 
			
		||||
  float position_;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from esphome.components import display, font
 | 
			
		||||
import esphome.components.image as espImage
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE
 | 
			
		||||
from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE
 | 
			
		||||
from esphome.core import CORE, HexInt
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -15,8 +15,6 @@ MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
Animation_ = display.display_ns.class_("Animation")
 | 
			
		||||
 | 
			
		||||
CONF_RAW_DATA_ID = "raw_data_id"
 | 
			
		||||
 | 
			
		||||
ANIMATION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.declare_id(Animation_),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
#include "anova.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "anova";
 | 
			
		||||
static const char *const TAG = "anova";
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
 | 
			
		||||
void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); }
 | 
			
		||||
 | 
			
		||||
void Anova::setup() {
 | 
			
		||||
  this->codec_ = new AnovaCodec();
 | 
			
		||||
  this->codec_ = make_unique<AnovaCodec>();
 | 
			
		||||
  this->current_request_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -72,7 +72,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      this->current_request_ = 0;
 | 
			
		||||
      this->update();
 | 
			
		||||
      break;
 | 
			
		||||
@@ -129,13 +129,13 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
 | 
			
		||||
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)
 | 
			
		||||
  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');
 | 
			
		||||
      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)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "anova_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +27,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
 | 
			
		||||
                           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() {
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supports_heat_mode(true);
 | 
			
		||||
@@ -39,7 +39,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
 | 
			
		||||
  void set_unit_of_measurement(const char *);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AnovaCodec *codec_;
 | 
			
		||||
  std::unique_ptr<AnovaCodec> codec_;
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
  uint16_t char_handle_;
 | 
			
		||||
  uint8_t current_request_;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
#include "anova_base.h"
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace anova {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "apds9960.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace apds9960 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ENCRYPTION): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_KEY): validate_encryption_key,
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -92,6 +116,15 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.register_user_service(trigger))
 | 
			
		||||
        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.3")
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add_define("USE_API_PLAINTEXT")
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_API")
 | 
			
		||||
    cg.add_global(api_ns.using)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -215,6 +215,7 @@ message ListEntitiesBinarySensorResponse {
 | 
			
		||||
  string device_class = 5;
 | 
			
		||||
  bool is_status_binary_sensor = 6;
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
  string icon = 8;
 | 
			
		||||
}
 | 
			
		||||
message BinarySensorStateResponse {
 | 
			
		||||
  option (id) = 21;
 | 
			
		||||
@@ -245,6 +246,7 @@ message ListEntitiesCoverResponse {
 | 
			
		||||
  bool supports_tilt = 7;
 | 
			
		||||
  string device_class = 8;
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
  string icon = 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum LegacyCoverState {
 | 
			
		||||
@@ -313,6 +315,7 @@ message ListEntitiesFanResponse {
 | 
			
		||||
  bool supports_direction = 7;
 | 
			
		||||
  int32 supported_speed_count = 8;
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
  string icon = 10;
 | 
			
		||||
}
 | 
			
		||||
enum FanSpeed {
 | 
			
		||||
  FAN_SPEED_LOW = 0;
 | 
			
		||||
@@ -388,6 +391,7 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  float max_mireds = 10;
 | 
			
		||||
  repeated string effects = 11;
 | 
			
		||||
  bool disabled_by_default = 13;
 | 
			
		||||
  string icon = 14;
 | 
			
		||||
}
 | 
			
		||||
message LightStateResponse {
 | 
			
		||||
  option (id) = 24;
 | 
			
		||||
@@ -473,7 +477,8 @@ message ListEntitiesSensorResponse {
 | 
			
		||||
  bool force_update = 8;
 | 
			
		||||
  string device_class = 9;
 | 
			
		||||
  SensorStateClass state_class = 10;
 | 
			
		||||
  SensorLastResetType last_reset_type = 11;
 | 
			
		||||
  // Last reset type removed in 2021.9.0
 | 
			
		||||
  SensorLastResetType legacy_last_reset_type = 11;
 | 
			
		||||
  bool disabled_by_default = 12;
 | 
			
		||||
}
 | 
			
		||||
message SensorStateResponse {
 | 
			
		||||
@@ -789,6 +794,7 @@ message ListEntitiesClimateResponse {
 | 
			
		||||
  repeated ClimatePreset supported_presets = 16;
 | 
			
		||||
  repeated string supported_custom_presets = 17;
 | 
			
		||||
  bool disabled_by_default = 18;
 | 
			
		||||
  string icon = 19;
 | 
			
		||||
}
 | 
			
		||||
message ClimateStateResponse {
 | 
			
		||||
  option (id) = 47;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
#include "api_connection.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
 | 
			
		||||
@@ -18,143 +21,144 @@ namespace api {
 | 
			
		||||
 | 
			
		||||
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()) {
 | 
			
		||||
  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();
 | 
			
		||||
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
 | 
			
		||||
  return App.get_name() + component_type + entity->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
 | 
			
		||||
@@ -177,6 +181,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_
 | 
			
		||||
  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();
 | 
			
		||||
  msg.icon = binary_sensor->get_icon();
 | 
			
		||||
  return this->send_list_entities_binary_sensor_response(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -209,6 +214,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) {
 | 
			
		||||
  msg.supports_tilt = traits.get_supports_tilt();
 | 
			
		||||
  msg.device_class = cover->get_device_class();
 | 
			
		||||
  msg.disabled_by_default = cover->is_disabled_by_default();
 | 
			
		||||
  msg.icon = cover->get_icon();
 | 
			
		||||
  return this->send_list_entities_cover_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
@@ -241,6 +247,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;
 | 
			
		||||
@@ -271,6 +280,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) {
 | 
			
		||||
  msg.supports_direction = traits.supports_direction();
 | 
			
		||||
  msg.supported_speed_count = traits.supported_speed_count();
 | 
			
		||||
  msg.disabled_by_default = fan->is_disabled_by_default();
 | 
			
		||||
  msg.icon = fan->get_icon();
 | 
			
		||||
  return this->send_list_entities_fan_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
@@ -295,6 +305,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
 | 
			
		||||
@@ -332,6 +343,7 @@ bool APIConnection::send_light_info(light::LightState *light) {
 | 
			
		||||
  msg.unique_id = get_default_unique_id("light", light);
 | 
			
		||||
 | 
			
		||||
  msg.disabled_by_default = light->is_disabled_by_default();
 | 
			
		||||
  msg.icon = light->get_icon();
 | 
			
		||||
 | 
			
		||||
  for (auto mode : traits.get_supported_color_modes())
 | 
			
		||||
    msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
 | 
			
		||||
@@ -416,8 +428,7 @@ 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->state_class);
 | 
			
		||||
  msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type);
 | 
			
		||||
  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);
 | 
			
		||||
@@ -523,6 +534,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
 | 
			
		||||
  msg.unique_id = get_default_unique_id("climate", climate);
 | 
			
		||||
 | 
			
		||||
  msg.disabled_by_default = climate->is_disabled_by_default();
 | 
			
		||||
  msg.icon = climate->get_icon();
 | 
			
		||||
 | 
			
		||||
  msg.supports_current_temperature = traits.get_supports_current_temperature();
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
 | 
			
		||||
@@ -595,7 +607,7 @@ bool APIConnection::send_number_info(number::Number *number) {
 | 
			
		||||
  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.icon = number->get_icon();
 | 
			
		||||
  msg.disabled_by_default = number->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  msg.min_value = number->traits.get_min_value();
 | 
			
		||||
@@ -632,7 +644,7 @@ bool APIConnection::send_select_info(select::Select *select) {
 | 
			
		||||
  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.icon = select->get_icon();
 | 
			
		||||
  msg.disabled_by_default = select->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  for (const auto &option : select->traits.get_options())
 | 
			
		||||
@@ -657,7 +669,7 @@ void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage>
 | 
			
		||||
    return;
 | 
			
		||||
  if (this->image_reader_.available())
 | 
			
		||||
    return;
 | 
			
		||||
  this->image_reader_.set_image(image);
 | 
			
		||||
  this->image_reader_.set_image(std::move(image));
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
 | 
			
		||||
  ListEntitiesCameraResponse msg;
 | 
			
		||||
@@ -697,20 +709,12 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
 | 
			
		||||
  // 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;
 | 
			
		||||
@@ -727,7 +731,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
 | 
			
		||||
@@ -745,9 +749,7 @@ 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
 | 
			
		||||
@@ -775,30 +777,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;
 | 
			
		||||
    resp.attribute = it.attribute.value();
 | 
			
		||||
    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");
 | 
			
		||||
@@ -808,24 +800,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() {
 | 
			
		||||
@@ -86,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;
 | 
			
		||||
@@ -100,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(); }
 | 
			
		||||
@@ -135,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,
 | 
			
		||||
@@ -157,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
 | 
			
		||||
@@ -170,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										998
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										998
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,998 @@
 | 
			
		||||
#include "api_frame_helper.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_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;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_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
 | 
			
		||||
							
								
								
									
										184
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#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_(std::move(std::move(ctx))) {}
 | 
			
		||||
  ~APINoiseFrameHelper() override;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  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() override = default;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  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_ = psk; }
 | 
			
		||||
  const psk_t &get_psk() const { return psk_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  psk_t psk_;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -531,6 +531,10 @@ bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLen
 | 
			
		||||
      this->device_class = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 8: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -553,6 +557,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(5, this->device_class);
 | 
			
		||||
  buffer.encode_bool(6, this->is_status_binary_sensor);
 | 
			
		||||
  buffer.encode_bool(7, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(8, this->icon);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -586,6 +591,10 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -677,6 +686,10 @@ bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDeli
 | 
			
		||||
      this->device_class = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 10: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -701,6 +714,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(7, this->supports_tilt);
 | 
			
		||||
  buffer.encode_string(8, this->device_class);
 | 
			
		||||
  buffer.encode_bool(9, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(10, this->icon);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesCoverResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -742,6 +756,10 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -948,6 +966,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi
 | 
			
		||||
      this->unique_id = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 10: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -972,6 +994,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(7, this->supports_direction);
 | 
			
		||||
  buffer.encode_int32(8, this->supported_speed_count);
 | 
			
		||||
  buffer.encode_bool(9, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(10, this->icon);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesFanResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1014,6 +1037,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1262,6 +1289,10 @@ bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDeli
 | 
			
		||||
      this->effects.push_back(value.as_string());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 14: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1302,6 +1333,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
    buffer.encode_string(11, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(13, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(14, this->icon);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesLightResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1365,6 +1397,10 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1817,7 +1853,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 11: {
 | 
			
		||||
      this->last_reset_type = value.as_enum<enums::SensorLastResetType>();
 | 
			
		||||
      this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 12: {
 | 
			
		||||
@@ -1879,7 +1915,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(8, this->force_update);
 | 
			
		||||
  buffer.encode_string(9, this->device_class);
 | 
			
		||||
  buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
 | 
			
		||||
  buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type);
 | 
			
		||||
  buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
 | 
			
		||||
  buffer.encode_bool(12, this->disabled_by_default);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1928,8 +1964,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  last_reset_type: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type));
 | 
			
		||||
  out.append("  legacy_last_reset_type: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
@@ -3072,6 +3108,10 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe
 | 
			
		||||
      this->supported_custom_presets.push_back(value.as_string());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 19: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -3129,6 +3169,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
    buffer.encode_string(17, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(18, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(19, this->icon);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -3221,6 +3262,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage {
 | 
			
		||||
  std::string device_class{};
 | 
			
		||||
  bool is_status_binary_sensor{false};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -304,6 +305,7 @@ class ListEntitiesCoverResponse : public ProtoMessage {
 | 
			
		||||
  bool supports_tilt{false};
 | 
			
		||||
  std::string device_class{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -360,6 +362,7 @@ class ListEntitiesFanResponse : public ProtoMessage {
 | 
			
		||||
  bool supports_direction{false};
 | 
			
		||||
  int32_t supported_speed_count{0};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -424,6 +427,7 @@ class ListEntitiesLightResponse : public ProtoMessage {
 | 
			
		||||
  float max_mireds{0.0f};
 | 
			
		||||
  std::vector<std::string> effects{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -510,7 +514,7 @@ class ListEntitiesSensorResponse : public ProtoMessage {
 | 
			
		||||
  bool force_update{false};
 | 
			
		||||
  std::string device_class{};
 | 
			
		||||
  enums::SensorStateClass state_class{};
 | 
			
		||||
  enums::SensorLastResetType last_reset_type{};
 | 
			
		||||
  enums::SensorLastResetType legacy_last_reset_type{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -856,6 +860,7 @@ class ListEntitiesClimateResponse : public ProtoMessage {
 | 
			
		||||
  std::vector<enums::ClimatePreset> supported_presets{};
 | 
			
		||||
  std::vector<std::string> supported_custom_presets{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
#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 "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
@@ -21,24 +24,49 @@ static const char *const TAG = "api";
 | 
			
		||||
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) {
 | 
			
		||||
      for (auto *c : this->clients_) {
 | 
			
		||||
      for (auto &c : this->clients_) {
 | 
			
		||||
        if (!c->remove_)
 | 
			
		||||
          c->send_log_message(level, tag, message);
 | 
			
		||||
      }
 | 
			
		||||
@@ -50,30 +78,41 @@ void APIServer::setup() {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  if (esp32_camera::global_esp32_camera != nullptr) {
 | 
			
		||||
    esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr<esp32_camera::CameraImage> image) {
 | 
			
		||||
      for (auto *c : this->clients_)
 | 
			
		||||
        if (!c->remove_)
 | 
			
		||||
          c->send_camera_state(image);
 | 
			
		||||
    });
 | 
			
		||||
    esp32_camera::global_esp32_camera->add_image_callback(
 | 
			
		||||
        [this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
 | 
			
		||||
          for (auto &c : this->clients_)
 | 
			
		||||
            if (!c->remove_)
 | 
			
		||||
              c->send_camera_state(image);
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
#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_.emplace_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_; });
 | 
			
		||||
  auto new_end = std::partition(this->clients_.begin(), this->clients_.end(),
 | 
			
		||||
                                [](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; });
 | 
			
		||||
  // print disconnection messages
 | 
			
		||||
  for (auto it = new_end; it != this->clients_.end(); ++it) {
 | 
			
		||||
    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
 | 
			
		||||
  for (auto it = new_end; it != this->clients_.end(); ++it)
 | 
			
		||||
    delete *it;
 | 
			
		||||
  // resize vector
 | 
			
		||||
  this->clients_.erase(new_end, this->clients_.end());
 | 
			
		||||
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->loop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +132,12 @@ void APIServer::loop() {
 | 
			
		||||
}
 | 
			
		||||
void APIServer::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "API Server:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->port_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network::get_use_address().c_str(), this->port_);
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Using noise encryption: YES");
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Using noise encryption: NO");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
 | 
			
		||||
bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
@@ -129,7 +173,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
			
		||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_binary_sensor_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -138,7 +182,7 @@ void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s
 | 
			
		||||
void APIServer::on_cover_update(cover::Cover *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_cover_state(obj);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -147,7 +191,7 @@ void APIServer::on_cover_update(cover::Cover *obj) {
 | 
			
		||||
void APIServer::on_fan_update(fan::FanState *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_fan_state(obj);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -156,7 +200,7 @@ void APIServer::on_fan_update(fan::FanState *obj) {
 | 
			
		||||
void APIServer::on_light_update(light::LightState *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_light_state(obj);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -165,7 +209,7 @@ void APIServer::on_light_update(light::LightState *obj) {
 | 
			
		||||
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_sensor_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -174,7 +218,7 @@ void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
 | 
			
		||||
void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_switch_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -183,7 +227,7 @@ void APIServer::on_switch_update(switch_::Switch *obj, bool 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_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_text_sensor_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -192,7 +236,7 @@ void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s
 | 
			
		||||
void APIServer::on_climate_update(climate::Climate *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_climate_state(obj);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -201,7 +245,7 @@ void APIServer::on_climate_update(climate::Climate *obj) {
 | 
			
		||||
void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_number_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -210,7 +254,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto *c : this->clients_)
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_select_state(obj, state);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -221,7 +265,7 @@ APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-c
 | 
			
		||||
 | 
			
		||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
 | 
			
		||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->send_homeassistant_service_call(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -241,7 +285,7 @@ uint16_t APIServer::get_port() const { return this->port_; }
 | 
			
		||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
void APIServer::request_time() {
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED)
 | 
			
		||||
      client->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
@@ -249,7 +293,7 @@ void APIServer::request_time() {
 | 
			
		||||
#endif
 | 
			
		||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
 | 
			
		||||
void APIServer::on_shutdown() {
 | 
			
		||||
  for (auto *c : this->clients_) {
 | 
			
		||||
  for (auto &c : this->clients_) {
 | 
			
		||||
    c->send_disconnect_request(DisconnectRequest());
 | 
			
		||||
  }
 | 
			
		||||
  delay(10);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +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 "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 {
 | 
			
		||||
@@ -35,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(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;
 | 
			
		||||
@@ -86,14 +87,18 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  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};
 | 
			
		||||
  std::vector<APIConnection *> clients_;
 | 
			
		||||
  std::vector<std::unique_ptr<APIConnection>> clients_;
 | 
			
		||||
  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;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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))
 | 
			
		||||
@@ -49,7 +49,7 @@ class CustomAPIDevice {
 | 
			
		||||
  template<typename T, typename... Ts>
 | 
			
		||||
  void register_service(void (T::*callback)(Ts...), const std::string &name,
 | 
			
		||||
                        const std::array<std::string, sizeof...(Ts)> &arg_names) {
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -72,7 +72,7 @@ class CustomAPIDevice {
 | 
			
		||||
   * @param name The name of the arguments for the service, must match the arguments of the function.
 | 
			
		||||
   */
 | 
			
		||||
  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -246,6 +246,7 @@ class ProtoWriteBuffer {
 | 
			
		||||
 | 
			
		||||
class ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual ~ProtoMessage() = default;
 | 
			
		||||
  virtual void encode(ProtoWriteBuffer buffer) const = 0;
 | 
			
		||||
  void decode(const uint8_t *buffer, size_t length);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,12 @@ void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits,
 | 
			
		||||
 | 
			
		||||
uint8_t I2CAS3935Component::read_register(uint8_t reg) {
 | 
			
		||||
  uint8_t value;
 | 
			
		||||
  if (!this->read_byte(reg, &value, 2)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Read failed!");
 | 
			
		||||
  if (write(®, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Writing register failed!");
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  if (read(&value, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Reading register failed!");
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
# Dummy integration to allow relying on AsyncTCP
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
    cv.only_with_arduino,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(200.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -12,4 +18,4 @@ async def to_code(config):
 | 
			
		||||
        cg.add_library("esphome/AsyncTCP-esphome", "1.2.2")
 | 
			
		||||
    elif CORE.is_esp8266:
 | 
			
		||||
        # https://github.com/OttoWinter/ESPAsyncTCP
 | 
			
		||||
        cg.add_library("ESPAsyncTCP-esphome", "1.2.3")
 | 
			
		||||
        cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#include "atc_mithermometer.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atc_mithermometer {
 | 
			
		||||
@@ -25,14 +25,14 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
 | 
			
		||||
 | 
			
		||||
  bool success = false;
 | 
			
		||||
  for (auto &service_data : device.get_service_datas()) {
 | 
			
		||||
    auto res = parse_header(service_data);
 | 
			
		||||
    if (res->is_duplicate) {
 | 
			
		||||
    auto res = parse_header_(service_data);
 | 
			
		||||
    if (!res.has_value()) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (!(parse_message(service_data.data, *res))) {
 | 
			
		||||
    if (!(parse_message_(service_data.data, *res))) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (!(report_results(res, device.address_str()))) {
 | 
			
		||||
    if (!(report_results_(res, device.address_str()))) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (res->temperature.has_value() && this->temperature_ != nullptr)
 | 
			
		||||
@@ -46,14 +46,10 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
 | 
			
		||||
    success = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!success) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
  return success;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<ParseResult> ATCMiThermometer::parse_header(const esp32_ble_tracker::ServiceData &service_data) {
 | 
			
		||||
optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::ServiceData &service_data) {
 | 
			
		||||
  ParseResult result;
 | 
			
		||||
  if (!service_data.uuid.contains(0x1A, 0x18)) {
 | 
			
		||||
    ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes.");
 | 
			
		||||
@@ -64,17 +60,15 @@ optional<ParseResult> ATCMiThermometer::parse_header(const esp32_ble_tracker::Se
 | 
			
		||||
 | 
			
		||||
  static uint8_t last_frame_count = 0;
 | 
			
		||||
  if (last_frame_count == raw[12]) {
 | 
			
		||||
    ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%d).", static_cast<int>(last_frame_count));
 | 
			
		||||
    result.is_duplicate = true;
 | 
			
		||||
    ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count);
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  last_frame_count = raw[12];
 | 
			
		||||
  result.is_duplicate = false;
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ATCMiThermometer::parse_message(const std::vector<uint8_t> &message, ParseResult &result) {
 | 
			
		||||
bool ATCMiThermometer::parse_message_(const std::vector<uint8_t> &message, ParseResult &result) {
 | 
			
		||||
  // Byte 0-5 mac in correct order
 | 
			
		||||
  // Byte 6-7 Temperature in uint16
 | 
			
		||||
  // Byte 8 Humidity in percent
 | 
			
		||||
@@ -107,7 +101,7 @@ bool ATCMiThermometer::parse_message(const std::vector<uint8_t> &message, ParseR
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ATCMiThermometer::report_results(const optional<ParseResult> &result, const std::string &address) {
 | 
			
		||||
bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, const std::string &address) {
 | 
			
		||||
  if (!result.has_value()) {
 | 
			
		||||
    ESP_LOGVV(TAG, "report_results(): no results available.");
 | 
			
		||||
    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atc_mithermometer {
 | 
			
		||||
@@ -14,7 +14,6 @@ struct ParseResult {
 | 
			
		||||
  optional<float> humidity;
 | 
			
		||||
  optional<float> battery_level;
 | 
			
		||||
  optional<float> battery_voltage;
 | 
			
		||||
  bool is_duplicate;
 | 
			
		||||
  int raw_offset;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -37,9 +36,9 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice
 | 
			
		||||
  sensor::Sensor *battery_level_{nullptr};
 | 
			
		||||
  sensor::Sensor *battery_voltage_{nullptr};
 | 
			
		||||
 | 
			
		||||
  optional<ParseResult> parse_header(const esp32_ble_tracker::ServiceData &service_data);
 | 
			
		||||
  bool parse_message(const std::vector<uint8_t> &message, ParseResult &result);
 | 
			
		||||
  bool report_results(const optional<ParseResult> &result, const std::string &address);
 | 
			
		||||
  optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data);
 | 
			
		||||
  bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result);
 | 
			
		||||
  bool report_results_(const optional<ParseResult> &result, const std::string &address);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace atc_mithermometer
 | 
			
		||||
 
 | 
			
		||||
@@ -265,27 +265,57 @@ float ATM90E32Component::get_power_factor_c_() {
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_forward_active_energy_a_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA);
 | 
			
		||||
  return (float) val * 10 / 3200;  // convert register value to WattHours
 | 
			
		||||
  if ((UINT32_MAX - this->phase_[0].cumulative_forward_active_energy_) > val) {
 | 
			
		||||
    this->phase_[0].cumulative_forward_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[0].cumulative_forward_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[0].cumulative_forward_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_forward_active_energy_b_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB);
 | 
			
		||||
  return (float) val * 10 / 3200;
 | 
			
		||||
  if (UINT32_MAX - this->phase_[1].cumulative_forward_active_energy_ > val) {
 | 
			
		||||
    this->phase_[1].cumulative_forward_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[1].cumulative_forward_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[1].cumulative_forward_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_forward_active_energy_c_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC);
 | 
			
		||||
  return (float) val * 10 / 3200;
 | 
			
		||||
  if (UINT32_MAX - this->phase_[2].cumulative_forward_active_energy_ > val) {
 | 
			
		||||
    this->phase_[2].cumulative_forward_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[2].cumulative_forward_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[2].cumulative_forward_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_reverse_active_energy_a_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA);
 | 
			
		||||
  return (float) val * 10 / 3200;
 | 
			
		||||
  if (UINT32_MAX - this->phase_[0].cumulative_reverse_active_energy_ > val) {
 | 
			
		||||
    this->phase_[0].cumulative_reverse_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[0].cumulative_reverse_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[0].cumulative_reverse_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_reverse_active_energy_b_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB);
 | 
			
		||||
  return (float) val * 10 / 3200;
 | 
			
		||||
  if (UINT32_MAX - this->phase_[1].cumulative_reverse_active_energy_ > val) {
 | 
			
		||||
    this->phase_[1].cumulative_reverse_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[1].cumulative_reverse_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[1].cumulative_reverse_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_reverse_active_energy_c_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC);
 | 
			
		||||
  return (float) val * 10 / 3200;
 | 
			
		||||
  if (UINT32_MAX - this->phase_[2].cumulative_reverse_active_energy_ > val) {
 | 
			
		||||
    this->phase_[2].cumulative_reverse_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[2].cumulative_reverse_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[2].cumulative_reverse_active_energy_ * 10 / 3200);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E32Component::get_frequency_() {
 | 
			
		||||
  uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ);
 | 
			
		||||
 
 | 
			
		||||
@@ -77,6 +77,8 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
    sensor::Sensor *power_factor_sensor_{nullptr};
 | 
			
		||||
    sensor::Sensor *forward_active_energy_sensor_{nullptr};
 | 
			
		||||
    sensor::Sensor *reverse_active_energy_sensor_{nullptr};
 | 
			
		||||
    uint32_t cumulative_forward_active_energy_{0};
 | 
			
		||||
    uint32_t cumulative_reverse_active_energy_{0};
 | 
			
		||||
  } phase_[3];
 | 
			
		||||
  sensor::Sensor *freq_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *chip_temperature_sensor_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,8 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
    ICON_CURRENT_AC,
 | 
			
		||||
    LAST_RESET_TYPE_AUTO,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
@@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
 | 
			
		||||
        cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#include "b_parasite.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace b_parasite {
 | 
			
		||||
@@ -14,6 +14,7 @@ void BParasite::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_);
 | 
			
		||||
  LOG_SENSOR("  ", "Soil Moisture", this->soil_moisture_);
 | 
			
		||||
  LOG_SENSOR("  ", "Illuminance", this->illuminance_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
@@ -36,6 +37,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
 | 
			
		||||
  const auto &data = service_data.data;
 | 
			
		||||
 | 
			
		||||
  const uint8_t protocol_version = data[0] >> 4;
 | 
			
		||||
  if (protocol_version != 1) {
 | 
			
		||||
    ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Some b-parasite versions have an (optional) illuminance sensor.
 | 
			
		||||
  bool has_illuminance = data[0] & 0x1;
 | 
			
		||||
 | 
			
		||||
  // Counter for deduplicating messages.
 | 
			
		||||
  uint8_t counter = data[1] & 0x0f;
 | 
			
		||||
  if (last_processed_counter_ == counter) {
 | 
			
		||||
@@ -59,6 +69,9 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  uint16_t soil_moisture = data[8] << 8 | data[9];
 | 
			
		||||
  float moisture_percent = (100.0f * soil_moisture) / (1 << 16);
 | 
			
		||||
 | 
			
		||||
  // Ambient light in lux.
 | 
			
		||||
  float illuminance = has_illuminance ? data[16] << 8 | data[17] : 0.0f;
 | 
			
		||||
 | 
			
		||||
  if (battery_voltage_ != nullptr) {
 | 
			
		||||
    battery_voltage_->publish_state(battery_voltage);
 | 
			
		||||
  }
 | 
			
		||||
@@ -71,6 +84,13 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  if (soil_moisture_ != nullptr) {
 | 
			
		||||
    soil_moisture_->publish_state(moisture_percent);
 | 
			
		||||
  }
 | 
			
		||||
  if (illuminance_ != nullptr) {
 | 
			
		||||
    if (has_illuminance) {
 | 
			
		||||
      illuminance_->publish_state(illuminance);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "No lux information is present in the BLE packet");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  last_processed_counter_ = counter;
 | 
			
		||||
  return true;
 | 
			
		||||
@@ -79,4 +99,4 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
}  // namespace b_parasite
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace b_parasite {
 | 
			
		||||
@@ -22,6 +22,7 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene
 | 
			
		||||
  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
 | 
			
		||||
  void set_soil_moisture(sensor::Sensor *soil_moisture) { soil_moisture_ = soil_moisture; }
 | 
			
		||||
  void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // The received advertisement packet contains an unsigned 4 bits wrap-around counter
 | 
			
		||||
@@ -32,9 +33,10 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene
 | 
			
		||||
  sensor::Sensor *temperature_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_{nullptr};
 | 
			
		||||
  sensor::Sensor *soil_moisture_{nullptr};
 | 
			
		||||
  sensor::Sensor *illuminance_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace b_parasite
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,17 @@ from esphome.const import (
 | 
			
		||||
    CONF_BATTERY_VOLTAGE,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    CONF_MOISTURE,
 | 
			
		||||
    CONF_MAC_ADDRESS,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_LUX,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
@@ -55,6 +58,12 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_LUX,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 | 
			
		||||
@@ -74,6 +83,7 @@ async def to_code(config):
 | 
			
		||||
        (CONF_HUMIDITY, var.set_humidity),
 | 
			
		||||
        (CONF_BATTERY_VOLTAGE, var.set_battery_voltage),
 | 
			
		||||
        (CONF_MOISTURE, var.set_soil_moisture),
 | 
			
		||||
        (CONF_ILLUMINANCE, var.set_illuminance),
 | 
			
		||||
    ]:
 | 
			
		||||
        if config_key in config:
 | 
			
		||||
            sens = await sensor.new_sensor(config[config_key])
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user