mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			1258 Commits
		
	
	
		
			jesserockz
			...
			api_dispat
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0e687ff0b7 | ||
| 
						 | 
					6d1d7f137f | ||
| 
						 | 
					38e16efa11 | ||
| 
						 | 
					5e2f0f7f5e | ||
| 
						 | 
					83c7afc46f | ||
| 
						 | 
					10753f0f99 | ||
| 
						 | 
					34a852d433 | ||
| 
						 | 
					3922fbdef7 | ||
| 
						 | 
					e5415abf20 | ||
| 
						 | 
					67e1a92cce | ||
| 
						 | 
					4c64511a15 | ||
| 
						 | 
					75f3e0900e | ||
| 
						 | 
					abd33c21bf | ||
| 
						 | 
					d592ba2c5e | ||
| 
						 | 
					321eba5184 | ||
| 
						 | 
					82b9ec53fd | ||
| 
						 | 
					b9262f967b | ||
| 
						 | 
					949fb9a890 | ||
| 
						 | 
					99952a701f | ||
| 
						 | 
					88878adb6c | ||
| 
						 | 
					17e3b49ebb | ||
| 
						 | 
					a217747f5d | ||
| 
						 | 
					790c9cbb84 | ||
| 
						 | 
					da5fb6e24f | ||
| 
						 | 
					a77439b4b7 | ||
| 
						 | 
					1a049bdcbb | ||
| 
						 | 
					79686239d3 | ||
| 
						 | 
					c934e84e21 | ||
| 
						 | 
					5e2f8cb018 | ||
| 
						 | 
					6bd0af6d85 | ||
| 
						 | 
					0f28a49822 | ||
| 
						 | 
					66d96646b1 | ||
| 
						 | 
					be4cf6505f | ||
| 
						 | 
					e8ea7825a9 | ||
| 
						 | 
					8c13eab731 | ||
| 
						 | 
					bf4cbb0aee | ||
| 
						 | 
					aaec4b7bd3 | ||
| 
						 | 
					7bddcd4f64 | ||
| 
						 | 
					af205a5267 | ||
| 
						 | 
					c2599d7719 | ||
| 
						 | 
					4ea6f23d9e | ||
| 
						 | 
					f23fd52a26 | ||
| 
						 | 
					cfd43c81fb | ||
| 
						 | 
					3dcba675b4 | ||
| 
						 | 
					bb51031ec6 | ||
| 
						 | 
					ecb99cbcce | ||
| 
						 | 
					0a514821c6 | ||
| 
						 | 
					074fbb522c | ||
| 
						 | 
					71d6ba242e | ||
| 
						 | 
					37ffd64b48 | ||
| 
						 | 
					ec65652567 | ||
| 
						 | 
					731613421d | ||
| 
						 | 
					58dfad4ed0 | ||
| 
						 | 
					7eb029f4b9 | ||
| 
						 | 
					8da8d938f0 | ||
| 
						 | 
					64ac0d2bde | ||
| 
						 | 
					7d3cdd15ad | ||
| 
						 | 
					53baf02087 | ||
| 
						 | 
					a0d2392344 | ||
| 
						 | 
					fb3c092eaa | ||
| 
						 | 
					c169cf1e77 | ||
| 
						 | 
					fa4d8e083a | ||
| 
						 | 
					2cfeccfd71 | ||
| 
						 | 
					f36ca93752 | ||
| 
						 | 
					dc8714c277 | ||
| 
						 | 
					90fcb5fbcd | ||
| 
						 | 
					932d0a5d8b | ||
| 
						 | 
					4cafa18fa4 | ||
| 
						 | 
					b12d7db5a7 | ||
| 
						 | 
					e84345594d | ||
| 
						 | 
					add7bec7f2 | ||
| 
						 | 
					db84d8e8dc | ||
| 
						 | 
					ad51e647af | ||
| 
						 | 
					c45901746b | ||
| 
						 | 
					033c469250 | ||
| 
						 | 
					0900fd3cea | ||
| 
						 | 
					ba8f3d3f63 | ||
| 
						 | 
					2759f3828e | ||
| 
						 | 
					f395767766 | ||
| 
						 | 
					2dc222aea6 | ||
| 
						 | 
					82d68c87e2 | ||
| 
						 | 
					f213657753 | ||
| 
						 | 
					e077e6cec7 | ||
| 
						 | 
					339a3270f6 | ||
| 
						 | 
					462b44ee23 | ||
| 
						 | 
					52d3dba89c | ||
| 
						 | 
					939d01dd99 | ||
| 
						 | 
					4900f7c7ca | ||
| 
						 | 
					48957aee8b | ||
| 
						 | 
					e355ce04f7 | ||
| 
						 | 
					758e5b89bb | ||
| 
						 | 
					3ffdd1d451 | ||
| 
						 | 
					4c1b8c8b96 | ||
| 
						 | 
					3ca956cd6a | ||
| 
						 | 
					2e24a11a1d | ||
| 
						 | 
					10a03ad538 | ||
| 
						 | 
					69839ec4dc | ||
| 
						 | 
					28a66d4bf0 | ||
| 
						 | 
					782d894801 | ||
| 
						 | 
					06dd731c78 | ||
| 
						 | 
					6af74302dc | ||
| 
						 | 
					03380a6ecd | ||
| 
						 | 
					8d8db11dd9 | ||
| 
						 | 
					28886a896b | ||
| 
						 | 
					05253991c2 | ||
| 
						 | 
					96f0fda477 | ||
| 
						 | 
					023fa4d220 | ||
| 
						 | 
					a1f63c0dfc | ||
| 
						 | 
					ef98f42e7e | ||
| 
						 | 
					737e1284af | ||
| 
						 | 
					8677918157 | ||
| 
						 | 
					629c891dfc | ||
| 
						 | 
					8e8ef83780 | ||
| 
						 | 
					2a15f35e9d | ||
| 
						 | 
					9bfa942cf2 | ||
| 
						 | 
					b00adbddce | ||
| 
						 | 
					a71030c4de | ||
| 
						 | 
					6bb32c2e61 | ||
| 
						 | 
					7bc2c685e0 | ||
| 
						 | 
					9205338cc8 | ||
| 
						 | 
					04336f7ba3 | ||
| 
						 | 
					6f64312d08 | ||
| 
						 | 
					79dfb86830 | ||
| 
						 | 
					453dc29540 | ||
| 
						 | 
					f4260d370c | ||
| 
						 | 
					655f9489a8 | ||
| 
						 | 
					4b3cc52afe | ||
| 
						 | 
					fd3f15637a | ||
| 
						 | 
					1311e1b8b0 | ||
| 
						 | 
					64e84872da | ||
| 
						 | 
					bc7379030e | ||
| 
						 | 
					ecfb6dc8ed | ||
| 
						 | 
					75d67af932 | ||
| 
						 | 
					845dad6ee7 | ||
| 
						 | 
					e2e86da64b | ||
| 
						 | 
					90ec63589f | ||
| 
						 | 
					ea308eaaa2 | ||
| 
						 | 
					87f1fac2bf | ||
| 
						 | 
					c23651527f | ||
| 
						 | 
					2cc263a707 | ||
| 
						 | 
					fb336718de | ||
| 
						 | 
					e2e35bf965 | ||
| 
						 | 
					bdd25c7268 | ||
| 
						 | 
					82c788d6ce | ||
| 
						 | 
					5167184cc7 | ||
| 
						 | 
					a5d1b11204 | ||
| 
						 | 
					dc8f2fd37e | ||
| 
						 | 
					7c85886ce8 | ||
| 
						 | 
					12f172436d | ||
| 
						 | 
					e69ac0478e | ||
| 
						 | 
					a45743c2b7 | ||
| 
						 | 
					ebe1531927 | ||
| 
						 | 
					a88a059c6a | ||
| 
						 | 
					d314cbb0d5 | ||
| 
						 | 
					4d75758eb2 | ||
| 
						 | 
					0eecc29039 | ||
| 
						 | 
					294fb67410 | ||
| 
						 | 
					2f1f098b47 | ||
| 
						 | 
					77be414261 | ||
| 
						 | 
					c34fc3c4c7 | ||
| 
						 | 
					8aac2f525e | ||
| 
						 | 
					f85dcdca4e | ||
| 
						 | 
					e7a1ef7aa1 | ||
| 
						 | 
					7c2d2ef5a3 | ||
| 
						 | 
					1449001747 | ||
| 
						 | 
					f245c74520 | ||
| 
						 | 
					da1658e4f9 | ||
| 
						 | 
					80f9352a79 | ||
| 
						 | 
					9ded501402 | ||
| 
						 | 
					3d6a1811c5 | ||
| 
						 | 
					a5ee047efb | ||
| 
						 | 
					fb0090dcdc | ||
| 
						 | 
					294bd4d042 | ||
| 
						 | 
					e99b8d2daf | ||
| 
						 | 
					6dbdeeb59b | ||
| 
						 | 
					82fd62e9dd | ||
| 
						 | 
					70f935d323 | ||
| 
						 | 
					0f3e6cccd9 | ||
| 
						 | 
					6ff323c56d | ||
| 
						 | 
					096ec79ef9 | ||
| 
						 | 
					bf5ba65558 | ||
| 
						 | 
					62088dfaed | ||
| 
						 | 
					dfcc3206f7 | ||
| 
						 | 
					e173b7f0c2 | ||
| 
						 | 
					f98e28a8a2 | ||
| 
						 | 
					f63557f2e7 | ||
| 
						 | 
					a353598961 | ||
| 
						 | 
					bc33b44648 | ||
| 
						 | 
					1579779967 | ||
| 
						 | 
					cc6ea4cd14 | ||
| 
						 | 
					303a8ff87a | ||
| 
						 | 
					7d3a11a735 | ||
| 
						 | 
					94b6344820 | ||
| 
						 | 
					40307c079c | ||
| 
						 | 
					debef6fde4 | ||
| 
						 | 
					0cda83d29c | ||
| 
						 | 
					32729c7ca7 | ||
| 
						 | 
					b7fca5488a | ||
| 
						 | 
					9c22772758 | ||
| 
						 | 
					1e72f07fdf | ||
| 
						 | 
					a592e96709 | ||
| 
						 | 
					3980339868 | ||
| 
						 | 
					afa66c17bd | ||
| 
						 | 
					be2988b1d7 | ||
| 
						 | 
					cf647f0c36 | ||
| 
						 | 
					385ed4ca0c | ||
| 
						 | 
					9188a8e326 | ||
| 
						 | 
					0efb6d55c8 | ||
| 
						 | 
					f748047b7b | ||
| 
						 | 
					49bc767bf4 | ||
| 
						 | 
					e12cc9a9a7 | ||
| 
						 | 
					8e4470cdff | ||
| 
						 | 
					bdb7e19fd0 | ||
| 
						 | 
					0fc3f0e162 | ||
| 
						 | 
					6fac66e63b | ||
| 
						 | 
					71e06ea1b6 | ||
| 
						 | 
					3df434fd55 | ||
| 
						 | 
					729b2b2873 | ||
| 
						 | 
					bc2adb6b5a | ||
| 
						 | 
					aaff086aeb | ||
| 
						 | 
					e4c0f18ee3 | ||
| 
						 | 
					9c09a271f2 | ||
| 
						 | 
					37578f3e22 | ||
| 
						 | 
					4649599592 | ||
| 
						 | 
					71f78e3a81 | ||
| 
						 | 
					f7ca26eef8 | ||
| 
						 | 
					0665fcea9e | ||
| 
						 | 
					cd2b50c27f | ||
| 
						 | 
					ca70f17b3b | ||
| 
						 | 
					a5e08aaf74 | ||
| 
						 | 
					947db4605a | ||
| 
						 | 
					481a00a0b5 | ||
| 
						 | 
					465019e510 | ||
| 
						 | 
					a4d5f39fb6 | ||
| 
						 | 
					5dd76966c3 | ||
| 
						 | 
					db86f87fc3 | ||
| 
						 | 
					e21334b7fa | ||
| 
						 | 
					ba4c268956 | ||
| 
						 | 
					068594be5e | ||
| 
						 | 
					0fd45fc86e | ||
| 
						 | 
					257fb98113 | ||
| 
						 | 
					f8922b3cca | ||
| 
						 | 
					b0b08f317b | ||
| 
						 | 
					2c4667fb46 | ||
| 
						 | 
					9eadfa21d8 | ||
| 
						 | 
					953fd24458 | ||
| 
						 | 
					1be171e084 | ||
| 
						 | 
					5c83b99e0c | ||
| 
						 | 
					743e611735 | ||
| 
						 | 
					35ff850894 | ||
| 
						 | 
					b666295b53 | ||
| 
						 | 
					96cf8d97ab | ||
| 
						 | 
					3c1a781a1c | ||
| 
						 | 
					00bd1b0a02 | ||
| 
						 | 
					b8482da421 | ||
| 
						 | 
					756ece9ff3 | ||
| 
						 | 
					4bb016fec3 | ||
| 
						 | 
					32f0322dec | ||
| 
						 | 
					1a1c13b722 | ||
| 
						 | 
					139453822b | ||
| 
						 | 
					7a33994666 | ||
| 
						 | 
					f381d9011b | ||
| 
						 | 
					96352f047d | ||
| 
						 | 
					5e7a1fea8c | ||
| 
						 | 
					64eb70444d | ||
| 
						 | 
					0f39b1c49a | ||
| 
						 | 
					e2d6363c68 | ||
| 
						 | 
					cdeef700c2 | ||
| 
						 | 
					86fd702841 | ||
| 
						 | 
					6c62d4a923 | ||
| 
						 | 
					6e42d009fb | ||
| 
						 | 
					7d7769ea5d | ||
| 
						 | 
					3908677fe2 | ||
| 
						 | 
					9799a2b636 | ||
| 
						 | 
					55c8129423 | ||
| 
						 | 
					099474053e | ||
| 
						 | 
					efafabed97 | ||
| 
						 | 
					d209739f85 | ||
| 
						 | 
					d463dd0f57 | ||
| 
						 | 
					c33c14a46f | ||
| 
						 | 
					2d0c109dc1 | ||
| 
						 | 
					825d0bed88 | ||
| 
						 | 
					cd1390916c | ||
| 
						 | 
					149bdaf146 | ||
| 
						 | 
					ad628c9cba | ||
| 
						 | 
					b88f87799e | ||
| 
						 | 
					1ff7cf1125 | ||
| 
						 | 
					31db6e51eb | ||
| 
						 | 
					681d9236f9 | ||
| 
						 | 
					8aa8af735d | ||
| 
						 | 
					943d0f103d | ||
| 
						 | 
					8b195d7f63 | ||
| 
						 | 
					649ad47e62 | ||
| 
						 | 
					93dc5765bb | ||
| 
						 | 
					b000b1b70c | ||
| 
						 | 
					8707b6e01a | ||
| 
						 | 
					34abd67f3e | ||
| 
						 | 
					45f1db9233 | ||
| 
						 | 
					beb4d1511a | ||
| 
						 | 
					adeceee71f | ||
| 
						 | 
					7d4b11d112 | ||
| 
						 | 
					6733cd4ed1 | ||
| 
						 | 
					07f361a404 | ||
| 
						 | 
					ae981ea7f2 | ||
| 
						 | 
					b7d0f5e36b | ||
| 
						 | 
					3cbce4df42 | ||
| 
						 | 
					7e77e40bda | ||
| 
						 | 
					305805256d | ||
| 
						 | 
					e36c669dc0 | ||
| 
						 | 
					71aff9bc60 | ||
| 
						 | 
					36d11c969f | ||
| 
						 | 
					f76ce5d3bb | ||
| 
						 | 
					0df454481e | ||
| 
						 | 
					83c1a30cfb | ||
| 
						 | 
					6cbd1479c6 | ||
| 
						 | 
					3e6e438920 | ||
| 
						 | 
					560886eb90 | ||
| 
						 | 
					340bb5cef6 | ||
| 
						 | 
					44a7c1d4a5 | ||
| 
						 | 
					519c49f175 | ||
| 
						 | 
					c96ffefa42 | ||
| 
						 | 
					490ca8ad5a | ||
| 
						 | 
					e385f87d6c | ||
| 
						 | 
					58de53123a | ||
| 
						 | 
					4f365c1716 | ||
| 
						 | 
					981177da23 | ||
| 
						 | 
					088bea9ccd | ||
| 
						 | 
					36350f179e | ||
| 
						 | 
					902f08c1bc | ||
| 
						 | 
					47ad206ccd | ||
| 
						 | 
					9f51546023 | ||
| 
						 | 
					f6d679f056 | ||
| 
						 | 
					93c45e88e7 | ||
| 
						 | 
					da189da9ae | ||
| 
						 | 
					c40a33cb48 | ||
| 
						 | 
					9846beee7d | ||
| 
						 | 
					81685f9132 | ||
| 
						 | 
					14123d25c2 | ||
| 
						 | 
					928819ffbd | ||
| 
						 | 
					7f2f9636f5 | ||
| 
						 | 
					b49fe146ad | ||
| 
						 | 
					98bbd4136b | ||
| 
						 | 
					d8d02f71ba | ||
| 
						 | 
					26980df2b9 | ||
| 
						 | 
					ffe39473d0 | ||
| 
						 | 
					6af8d152ee | ||
| 
						 | 
					de846a8f7a | ||
| 
						 | 
					8e31316e3d | ||
| 
						 | 
					fb6edb3243 | ||
| 
						 | 
					244bd9256f | ||
| 
						 | 
					1f61fd383c | ||
| 
						 | 
					ce294ce0c1 | ||
| 
						 | 
					dcbdc0ac51 | ||
| 
						 | 
					daea06586d | ||
| 
						 | 
					9c8bf2587b | ||
| 
						 | 
					9871cb04ea | ||
| 
						 | 
					7dc093815f | ||
| 
						 | 
					087697106c | ||
| 
						 | 
					9beebc7bfe | ||
| 
						 | 
					4a948b7aae | ||
| 
						 | 
					0d3bc21e97 | ||
| 
						 | 
					7496894ae6 | ||
| 
						 | 
					918d7217a9 | ||
| 
						 | 
					2103d583f9 | ||
| 
						 | 
					837c446926 | ||
| 
						 | 
					480ea54ee0 | ||
| 
						 | 
					97e7c34cb6 | ||
| 
						 | 
					fe65b149f5 | ||
| 
						 | 
					4106b97174 | ||
| 
						 | 
					8648954b94 | ||
| 
						 | 
					9f1fae0955 | ||
| 
						 | 
					1d631c3c6d | ||
| 
						 | 
					727161f1db | ||
| 
						 | 
					bf5f628769 | ||
| 
						 | 
					8563a5785f | ||
| 
						 | 
					4082634e6d | ||
| 
						 | 
					a74adb5865 | ||
| 
						 | 
					2e4d7301f2 | ||
| 
						 | 
					94845222ad | ||
| 
						 | 
					7f6ac2deee | ||
| 
						 | 
					a054aa9c52 | ||
| 
						 | 
					22cb59b88c | ||
| 
						 | 
					6968772a31 | ||
| 
						 | 
					004f4b51d1 | ||
| 
						 | 
					8c8dd7b4bc | ||
| 
						 | 
					9778289d33 | ||
| 
						 | 
					a43caf08a6 | ||
| 
						 | 
					01e550fac9 | ||
| 
						 | 
					ad4dd6a060 | ||
| 
						 | 
					849d99b0dc | ||
| 
						 | 
					f5df5f71a3 | ||
| 
						 | 
					429be0a5ae | ||
| 
						 | 
					148e4ec555 | ||
| 
						 | 
					bb22f4d6a3 | ||
| 
						 | 
					f94703360b | ||
| 
						 | 
					f26bec1a5a | ||
| 
						 | 
					d065f4ae62 | ||
| 
						 | 
					ed2c3e626b | ||
| 
						 | 
					1927f92358 | ||
| 
						 | 
					939144174c | ||
| 
						 | 
					59bcbe7fef | ||
| 
						 | 
					8e00fedc67 | ||
| 
						 | 
					0ac879ae0b | ||
| 
						 | 
					22d1a18d22 | ||
| 
						 | 
					ca203bff9b | ||
| 
						 | 
					e01d16ce82 | ||
| 
						 | 
					93b6b9835c | ||
| 
						 | 
					d0ac5388d9 | ||
| 
						 | 
					9097d646ca | ||
| 
						 | 
					596a28e1fb | ||
| 
						 | 
					5205ff5c43 | ||
| 
						 | 
					c420bf5f4f | ||
| 
						 | 
					18844e15dc | ||
| 
						 | 
					af2f5b7348 | ||
| 
						 | 
					bcbf0f0e26 | ||
| 
						 | 
					4d460d4bc3 | ||
| 
						 | 
					92f6f3ac0d | ||
| 
						 | 
					bc63d246c8 | ||
| 
						 | 
					b25f272d72 | ||
| 
						 | 
					e3a3305adb | ||
| 
						 | 
					c655c4e106 | ||
| 
						 | 
					7fe8cdaa34 | ||
| 
						 | 
					df97985048 | ||
| 
						 | 
					3779675816 | ||
| 
						 | 
					0005aad5b5 | ||
| 
						 | 
					98c18517e2 | ||
| 
						 | 
					e4dee935ce | ||
| 
						 | 
					f8cb44fb3c | ||
| 
						 | 
					101901fdb8 | ||
| 
						 | 
					b8579d2040 | ||
| 
						 | 
					3fca3df756 | ||
| 
						 | 
					2f5db85997 | ||
| 
						 | 
					e0d4361875 | ||
| 
						 | 
					30bafc43bd | ||
| 
						 | 
					3530437b48 | ||
| 
						 | 
					81db42942c | ||
| 
						 | 
					6cb0d9e0b5 | ||
| 
						 | 
					19f7e36753 | ||
| 
						 | 
					a963f97520 | ||
| 
						 | 
					ad2d48e9b7 | ||
| 
						 | 
					5c0d67ca14 | ||
| 
						 | 
					3467329a7c | ||
| 
						 | 
					d73fa370f3 | ||
| 
						 | 
					78fd0a4870 | ||
| 
						 | 
					3162bb475d | ||
| 
						 | 
					c17503abd5 | ||
| 
						 | 
					3433ee8171 | ||
| 
						 | 
					344297b0a7 | ||
| 
						 | 
					947456628e | ||
| 
						 | 
					80dd6c111d | ||
| 
						 | 
					b70188ba4b | ||
| 
						 | 
					c6064aa2b4 | ||
| 
						 | 
					6596f864be | ||
| 
						 | 
					f61a40efb8 | ||
| 
						 | 
					b049f0b480 | ||
| 
						 | 
					b2641d29c1 | ||
| 
						 | 
					7b8cfc768d | ||
| 
						 | 
					04860567f7 | ||
| 
						 | 
					b16edb5a99 | ||
| 
						 | 
					15a995b2e7 | ||
| 
						 | 
					f57e26c54e | ||
| 
						 | 
					2b7bc1cd9f | ||
| 
						 | 
					614a2f66a3 | ||
| 
						 | 
					9047b02c92 | ||
| 
						 | 
					e73d0477bb | ||
| 
						 | 
					2b1e623eb4 | ||
| 
						 | 
					c366d555e9 | ||
| 
						 | 
					7efbd62730 | ||
| 
						 | 
					b77c1d0af8 | ||
| 
						 | 
					f8810ea6a8 | ||
| 
						 | 
					40dd667211 | ||
| 
						 | 
					848b572864 | ||
| 
						 | 
					7c858fbccd | ||
| 
						 | 
					a1814ea37d | ||
| 
						 | 
					5892a1dbe2 | ||
| 
						 | 
					29f524f432 | ||
| 
						 | 
					4ec588ebd7 | ||
| 
						 | 
					efdef61477 | ||
| 
						 | 
					fe2b9f8c12 | ||
| 
						 | 
					c6be55eb55 | ||
| 
						 | 
					4c69925b84 | ||
| 
						 | 
					bc6407df0a | ||
| 
						 | 
					01982a8d0a | ||
| 
						 | 
					b995cd6257 | ||
| 
						 | 
					b16d7b7a95 | ||
| 
						 | 
					42aea701d3 | ||
| 
						 | 
					5f56c85182 | ||
| 
						 | 
					52b4eb8950 | ||
| 
						 | 
					eeb2b42a0f | ||
| 
						 | 
					90772033d1 | ||
| 
						 | 
					dadeb4d2a9 | ||
| 
						 | 
					60a5029c88 | ||
| 
						 | 
					d7ba16b48b | ||
| 
						 | 
					fca9befa63 | ||
| 
						 | 
					187cbde0db | ||
| 
						 | 
					f5ae5cade8 | ||
| 
						 | 
					3e66c28aff | ||
| 
						 | 
					89703a1aef | ||
| 
						 | 
					cba31617e9 | ||
| 
						 | 
					a3eeb46961 | ||
| 
						 | 
					128bd76f20 | ||
| 
						 | 
					c0355fd2c6 | ||
| 
						 | 
					a5fd440e25 | ||
| 
						 | 
					592ef8be2a | ||
| 
						 | 
					3bcc1c7297 | ||
| 
						 | 
					3b44c3acd1 | ||
| 
						 | 
					ec4911643a | ||
| 
						 | 
					f4fedbab44 | ||
| 
						 | 
					553d441ecc | ||
| 
						 | 
					1946116438 | ||
| 
						 | 
					ab28515fba | ||
| 
						 | 
					4dc11fb95e | ||
| 
						 | 
					e27094e0f3 | ||
| 
						 | 
					88302201eb | ||
| 
						 | 
					8afb172e83 | ||
| 
						 | 
					562d024623 | ||
| 
						 | 
					50b094547c | ||
| 
						 | 
					a6c1e50985 | ||
| 
						 | 
					96772bdfc6 | ||
| 
						 | 
					ed154d373c | ||
| 
						 | 
					a5e862ce36 | ||
| 
						 | 
					ae55964bd9 | ||
| 
						 | 
					c162309f41 | ||
| 
						 | 
					62c667f1a0 | ||
| 
						 | 
					3d08eae8e4 | ||
| 
						 | 
					2af5a0a6dd | ||
| 
						 | 
					6d24b04235 | ||
| 
						 | 
					3ee8103353 | ||
| 
						 | 
					1296165fce | ||
| 
						 | 
					7100c22dc4 | ||
| 
						 | 
					5718c0f5b8 | ||
| 
						 | 
					25ebddfa1c | ||
| 
						 | 
					2c0558fe23 | ||
| 
						 | 
					7192108fc1 | ||
| 
						 | 
					847696c342 | ||
| 
						 | 
					912ae1fc87 | ||
| 
						 | 
					a86f75d31d | ||
| 
						 | 
					fe1e25b5c7 | ||
| 
						 | 
					9b241b596a | ||
| 
						 | 
					53b9c8d5bb | ||
| 
						 | 
					2946bc9d72 | ||
| 
						 | 
					67a20e212d | ||
| 
						 | 
					a9ace366eb | ||
| 
						 | 
					df3469efba | ||
| 
						 | 
					0a3bbb8554 | ||
| 
						 | 
					a15b9f5d3b | ||
| 
						 | 
					e6334b0716 | ||
| 
						 | 
					7a835baa5a | ||
| 
						 | 
					c9c21a5728 | ||
| 
						 | 
					956959fc32 | ||
| 
						 | 
					6f67f74638 | ||
| 
						 | 
					b3dd4543b7 | ||
| 
						 | 
					4f17a28ac5 | ||
| 
						 | 
					90736f367a | ||
| 
						 | 
					9af88bd482 | ||
| 
						 | 
					13b89f4934 | ||
| 
						 | 
					d00a00d142 | ||
| 
						 | 
					e662c39e16 | ||
| 
						 | 
					95ef131285 | ||
| 
						 | 
					7476f170f6 | ||
| 
						 | 
					3b6bd55d1e | ||
| 
						 | 
					10dbc9e884 | ||
| 
						 | 
					860f619dfe | ||
| 
						 | 
					17ddc9ee0c | ||
| 
						 | 
					949689c318 | ||
| 
						 | 
					86a2aac011 | ||
| 
						 | 
					d0a402f201 | ||
| 
						 | 
					05772d5365 | ||
| 
						 | 
					c2a68f5147 | ||
| 
						 | 
					697ca1c7be | ||
| 
						 | 
					409346952f | ||
| 
						 | 
					f4b3539d77 | ||
| 
						 | 
					c12166c1a1 | ||
| 
						 | 
					8d20f003cb | ||
| 
						 | 
					88f857a2f0 | ||
| 
						 | 
					fb7faadd99 | ||
| 
						 | 
					5c8d6752fb | ||
| 
						 | 
					dda81fbc2c | ||
| 
						 | 
					c40dff5d63 | ||
| 
						 | 
					6f07b54772 | ||
| 
						 | 
					ce0f1dfcb6 | ||
| 
						 | 
					9a3a5d48eb | ||
| 
						 | 
					4a759eda02 | ||
| 
						 | 
					26badf201d | ||
| 
						 | 
					384f27cd6d | ||
| 
						 | 
					ac1c5f9f58 | ||
| 
						 | 
					8ad058fdf4 | ||
| 
						 | 
					9024c3c67a | ||
| 
						 | 
					fc81a47499 | ||
| 
						 | 
					a331452076 | ||
| 
						 | 
					b1c6e8168e | ||
| 
						 | 
					b41cc0226e | ||
| 
						 | 
					450429ddd5 | ||
| 
						 | 
					f7b24f4b4b | ||
| 
						 | 
					294c985380 | ||
| 
						 | 
					720964b901 | ||
| 
						 | 
					8895c8a987 | ||
| 
						 | 
					740dcd72a2 | ||
| 
						 | 
					ffd442624f | ||
| 
						 | 
					088fd85694 | ||
| 
						 | 
					d5b68d69d3 | ||
| 
						 | 
					bb0f7bb393 | ||
| 
						 | 
					d86a108f18 | ||
| 
						 | 
					7828ed2d9e | ||
| 
						 | 
					ebf14f50fb | ||
| 
						 | 
					1546ff615b | ||
| 
						 | 
					46cf1fb597 | ||
| 
						 | 
					8bf8655054 | ||
| 
						 | 
					a6d84948e2 | ||
| 
						 | 
					fac20a1f97 | ||
| 
						 | 
					c65586b5e1 | ||
| 
						 | 
					b27b018b06 | ||
| 
						 | 
					403da1e632 | ||
| 
						 | 
					2371ec1f9e | ||
| 
						 | 
					5e3ec2d34b | ||
| 
						 | 
					78d84644c9 | ||
| 
						 | 
					0cd0f8015a | ||
| 
						 | 
					4b5424f695 | ||
| 
						 | 
					a1d59040f7 | ||
| 
						 | 
					0306398072 | ||
| 
						 | 
					a7e0bf9013 | ||
| 
						 | 
					ddb988cd83 | ||
| 
						 | 
					04b54353f1 | ||
| 
						 | 
					f058107c05 | ||
| 
						 | 
					6b5b0815d7 | ||
| 
						 | 
					8388497038 | ||
| 
						 | 
					825b1113b6 | ||
| 
						 | 
					9074ef792f | ||
| 
						 | 
					0946f28511 | ||
| 
						 | 
					23765cd4f5 | ||
| 
						 | 
					e20c6468d0 | ||
| 
						 | 
					b90516de1d | ||
| 
						 | 
					ec5cc0f00f | ||
| 
						 | 
					5dda5a976e | ||
| 
						 | 
					915da9ae13 | ||
| 
						 | 
					8652464f4e | ||
| 
						 | 
					ce6ce1c1f8 | ||
| 
						 | 
					39efe67e55 | ||
| 
						 | 
					748ffa00f3 | ||
| 
						 | 
					e8d9df2b0e | ||
| 
						 | 
					17396d67de | ||
| 
						 | 
					edd6a86714 | ||
| 
						 | 
					85b4012c56 | ||
| 
						 | 
					7d98433502 | ||
| 
						 | 
					23774ae03b | ||
| 
						 | 
					0dedbcdd71 | ||
| 
						 | 
					4bdd08887e | ||
| 
						 | 
					1fd8ebf386 | ||
| 
						 | 
					d2fc3e749c | ||
| 
						 | 
					71fbcbceaf | ||
| 
						 | 
					27347b2088 | ||
| 
						 | 
					599993d1a5 | ||
| 
						 | 
					bf359cb8e3 | ||
| 
						 | 
					509a704410 | ||
| 
						 | 
					1f48e2b01f | ||
| 
						 | 
					8b25b1eee6 | ||
| 
						 | 
					3bbf30ff5f | ||
| 
						 | 
					83613726d1 | ||
| 
						 | 
					254b6a17f3 | ||
| 
						 | 
					796e12bd70 | ||
| 
						 | 
					ddbe17d3f6 | ||
| 
						 | 
					591ec36f4a | ||
| 
						 | 
					41eceb72ef | ||
| 
						 | 
					0a5f094025 | ||
| 
						 | 
					ca0f3ba262 | ||
| 
						 | 
					30f4e782db | ||
| 
						 | 
					192158ef1a | ||
| 
						 | 
					602456db40 | ||
| 
						 | 
					536e45668f | ||
| 
						 | 
					10bf05ab0d | ||
| 
						 | 
					5ad1af69e4 | ||
| 
						 | 
					48f2911434 | ||
| 
						 | 
					dbb0d6349a | ||
| 
						 | 
					ac3598f12a | ||
| 
						 | 
					66201be5ca | ||
| 
						 | 
					ac0b0b652e | ||
| 
						 | 
					d89ee2df42 | ||
| 
						 | 
					418e248e5e | ||
| 
						 | 
					8c2b141049 | ||
| 
						 | 
					2f8e07302b | ||
| 
						 | 
					c3776240b6 | ||
| 
						 | 
					e370872ec1 | ||
| 
						 | 
					d4e978369a | ||
| 
						 | 
					8d5d7f5237 | ||
| 
						 | 
					5cd498fbe9 | ||
| 
						 | 
					250f515f08 | ||
| 
						 | 
					0ec0a9e313 | ||
| 
						 | 
					184f42ef03 | ||
| 
						 | 
					499517418d | ||
| 
						 | 
					606b9c1a6d | ||
| 
						 | 
					971e954a54 | ||
| 
						 | 
					e3aaf3219d | ||
| 
						 | 
					0eea1c0e40 | ||
| 
						 | 
					0773819778 | ||
| 
						 | 
					170869b7db | ||
| 
						 | 
					5dc54782e5 | ||
| 
						 | 
					97b26fbefe | ||
| 
						 | 
					686cc58d6c | ||
| 
						 | 
					76a59759b2 | ||
| 
						 | 
					93245a24b5 | ||
| 
						 | 
					6a22ea1c7d | ||
| 
						 | 
					56a02409c8 | ||
| 
						 | 
					edeafd5a53 | ||
| 
						 | 
					f67490b69b | ||
| 
						 | 
					b76e34fb7b | ||
| 
						 | 
					ddbda5032b | ||
| 
						 | 
					5898d34b0a | ||
| 
						 | 
					b0c02341ff | ||
| 
						 | 
					19cbc8c33b | ||
| 
						 | 
					02e61ef5d3 | ||
| 
						 | 
					8d5d18064d | ||
| 
						 | 
					c5ef7ebd27 | ||
| 
						 | 
					047a3e0e8c | ||
| 
						 | 
					13b23f840b | ||
| 
						 | 
					147f6012b2 | ||
| 
						 | 
					2c315595f0 | ||
| 
						 | 
					20405c84ac | ||
| 
						 | 
					0bc59b97de | ||
| 
						 | 
					a3a3bdc7eb | ||
| 
						 | 
					e767f30886 | ||
| 
						 | 
					e8c250a03c | ||
| 
						 | 
					d6725fc1ca | ||
| 
						 | 
					8ec998ff30 | ||
| 
						 | 
					23cc0c7f39 | ||
| 
						 | 
					19b8bd6aa8 | ||
| 
						 | 
					ed57e7c6b0 | ||
| 
						 | 
					9f489c9f27 | ||
| 
						 | 
					f036989361 | ||
| 
						 | 
					6afa8141c0 | ||
| 
						 | 
					587964c6f1 | ||
| 
						 | 
					7aea82a273 | ||
| 
						 | 
					20f946ccaf | ||
| 
						 | 
					e5e972231c | ||
| 
						 | 
					bfa80157f2 | ||
| 
						 | 
					99b1b079d0 | ||
| 
						 | 
					5697d549a8 | ||
| 
						 | 
					754d2874e7 | ||
| 
						 | 
					06de58ff8b | ||
| 
						 | 
					a0b3527710 | ||
| 
						 | 
					df24f48fa1 | ||
| 
						 | 
					13d53590b2 | ||
| 
						 | 
					5857f7b9a7 | ||
| 
						 | 
					a5ea0cd41f | ||
| 
						 | 
					d677934417 | ||
| 
						 | 
					ba87a0b63c | ||
| 
						 | 
					b725bb3dd1 | ||
| 
						 | 
					c34ba3deb5 | ||
| 
						 | 
					68b13340fb | ||
| 
						 | 
					8831999ea6 | ||
| 
						 | 
					c1853f8b84 | ||
| 
						 | 
					2b9b7e2853 | ||
| 
						 | 
					d3b18debf9 | ||
| 
						 | 
					b01eb28d42 | ||
| 
						 | 
					02019dd16c | ||
| 
						 | 
					7be12f5ff6 | ||
| 
						 | 
					a90d59b6ba | ||
| 
						 | 
					e7fa156254 | ||
| 
						 | 
					a8ab6b1c43 | ||
| 
						 | 
					25ed7c890b | ||
| 
						 | 
					85e3b63f05 | ||
| 
						 | 
					a37bac1956 | ||
| 
						 | 
					818a978dfc | ||
| 
						 | 
					180aeb7d8e | ||
| 
						 | 
					0764fa7292 | ||
| 
						 | 
					17bf533ed7 | ||
| 
						 | 
					d7eae1c1a0 | ||
| 
						 | 
					7f2d979255 | ||
| 
						 | 
					46b419ea8b | ||
| 
						 | 
					b30b527ff9 | ||
| 
						 | 
					41b1bfc504 | ||
| 
						 | 
					f4f14a7507 | ||
| 
						 | 
					61c29213a7 | ||
| 
						 | 
					e6d7639209 | ||
| 
						 | 
					3c07a186b2 | ||
| 
						 | 
					8a725250a9 | ||
| 
						 | 
					502b8a6073 | ||
| 
						 | 
					6212c6f80f | ||
| 
						 | 
					b03e3b8d4a | ||
| 
						 | 
					a98e34d190 | ||
| 
						 | 
					bf8d8b6e63 | ||
| 
						 | 
					57599f7a98 | ||
| 
						 | 
					ffccce7ffc | ||
| 
						 | 
					bbd5d050a9 | ||
| 
						 | 
					71a96fdcbf | ||
| 
						 | 
					221e3c6c9c | ||
| 
						 | 
					fb1679d572 | ||
| 
						 | 
					c19065f112 | ||
| 
						 | 
					f2b04a077e | ||
| 
						 | 
					8e7841c880 | ||
| 
						 | 
					1873490b24 | ||
| 
						 | 
					4d231953f4 | ||
| 
						 | 
					aa4c399657 | ||
| 
						 | 
					1f99d18982 | ||
| 
						 | 
					be37178ef8 | ||
| 
						 | 
					fad86c655e | ||
| 
						 | 
					4a7958586e | ||
| 
						 | 
					f44ecd0891 | ||
| 
						 | 
					3d0392d668 | ||
| 
						 | 
					d300d2605b | ||
| 
						 | 
					66cce6a2f2 | ||
| 
						 | 
					65e3c6bfbb | ||
| 
						 | 
					2a39060912 | ||
| 
						 | 
					8714e80978 | ||
| 
						 | 
					98de53f60b | ||
| 
						 | 
					41e11e9a0e | ||
| 
						 | 
					e7a4eac8bd | ||
| 
						 | 
					1589a131db | ||
| 
						 | 
					7d84f0e650 | ||
| 
						 | 
					86fb0e317f | ||
| 
						 | 
					32088d5ef7 | ||
| 
						 | 
					63de88dd57 | ||
| 
						 | 
					153a6440dc | ||
| 
						 | 
					8937ed2269 | ||
| 
						 | 
					02e922b56f | ||
| 
						 | 
					bf9e901ab9 | ||
| 
						 | 
					1234ef8de2 | ||
| 
						 | 
					41697a7b1b | ||
| 
						 | 
					912e265bc0 | ||
| 
						 | 
					96ee6fb064 | ||
| 
						 | 
					788dba8ef3 | ||
| 
						 | 
					fdde9c4681 | ||
| 
						 | 
					f195e73d38 | ||
| 
						 | 
					b0d9ffc6a1 | ||
| 
						 | 
					e17619841d | ||
| 
						 | 
					eb6a7cf3b9 | ||
| 
						 | 
					9901e2d72e | ||
| 
						 | 
					1be4e23b68 | ||
| 
						 | 
					e78094cc0a | ||
| 
						 | 
					bcf961c0b0 | ||
| 
						 | 
					f84a4c9753 | ||
| 
						 | 
					df56ca0236 | ||
| 
						 | 
					de0cd0ec67 | ||
| 
						 | 
					67c30245c4 | ||
| 
						 | 
					1f72757591 | ||
| 
						 | 
					35c2fdf6af | ||
| 
						 | 
					d1ecd841be | ||
| 
						 | 
					828a49697c | ||
| 
						 | 
					0551495501 | ||
| 
						 | 
					2bbffe4a68 | ||
| 
						 | 
					281ad90e39 | ||
| 
						 | 
					ed50976a07 | ||
| 
						 | 
					a3400037d9 | ||
| 
						 | 
					f0d82f75bc | ||
| 
						 | 
					349cb80e90 | ||
| 
						 | 
					c263ee39af | ||
| 
						 | 
					e99bc52756 | ||
| 
						 | 
					7944b2b8e9 | ||
| 
						 | 
					ca6ae746c1 | ||
| 
						 | 
					deabac18b2 | ||
| 
						 | 
					5cf8681c61 | ||
| 
						 | 
					ca7ede8f96 | ||
| 
						 | 
					4969682d52 | ||
| 
						 | 
					8002fe0dd5 | ||
| 
						 | 
					7dfdf965b7 | ||
| 
						 | 
					b408795dd6 | ||
| 
						 | 
					a5a099336b | ||
| 
						 | 
					4ae56fc004 | ||
| 
						 | 
					3f71c09b7b | ||
| 
						 | 
					bd50a7f1ab | ||
| 
						 | 
					51e4c45e5c | ||
| 
						 | 
					e3fae49add | ||
| 
						 | 
					610215ab60 | ||
| 
						 | 
					74acbda435 | ||
| 
						 | 
					25c4af777c | ||
| 
						 | 
					ec186e6324 | ||
| 
						 | 
					150b7a98f3 | ||
| 
						 | 
					8ae7c1cff0 | ||
| 
						 | 
					7f1d0eef98 | ||
| 
						 | 
					1179ab33f2 | ||
| 
						 | 
					a09faa1c10 | ||
| 
						 | 
					c0319d9b2f | ||
| 
						 | 
					4870cd2921 | ||
| 
						 | 
					d4280ec68b | ||
| 
						 | 
					52cdc11927 | ||
| 
						 | 
					8345b8c9ce | ||
| 
						 | 
					c56f0677c3 | ||
| 
						 | 
					00e9e1421e | ||
| 
						 | 
					93c72c6e6c | ||
| 
						 | 
					9cea930dbd | ||
| 
						 | 
					7b9bd70729 | ||
| 
						 | 
					5115c7a100 | ||
| 
						 | 
					5634494e64 | ||
| 
						 | 
					aa8bd4abf1 | ||
| 
						 | 
					17fd69dd7f | ||
| 
						 | 
					1d9dae374b | ||
| 
						 | 
					cb2241ad91 | ||
| 
						 | 
					d8a7e9abc8 | ||
| 
						 | 
					969abc3f29 | ||
| 
						 | 
					766fdc8a1f | ||
| 
						 | 
					4c37c20d76 | ||
| 
						 | 
					7d314398e1 | ||
| 
						 | 
					b69191e3a8 | ||
| 
						 | 
					b27c6b3596 | ||
| 
						 | 
					5453835963 | ||
| 
						 | 
					4d55ba057c | ||
| 
						 | 
					325c01242c | ||
| 
						 | 
					45b32bca89 | ||
| 
						 | 
					7620049214 | ||
| 
						 | 
					3553495a60 | ||
| 
						 | 
					3ce6db61d5 | ||
| 
						 | 
					798ff32c40 | ||
| 
						 | 
					430cee8bda | ||
| 
						 | 
					1fe3fb25a6 | ||
| 
						 | 
					685ed87581 | ||
| 
						 | 
					ea3ea1eee7 | ||
| 
						 | 
					c9edcb909b | ||
| 
						 | 
					35bfc9f069 | ||
| 
						 | 
					c4aec194b9 | ||
| 
						 | 
					e8547b16f6 | ||
| 
						 | 
					2bbe08cee0 | ||
| 
						 | 
					0a0c369b88 | ||
| 
						 | 
					5d2f454a94 | ||
| 
						 | 
					04bcc5c879 | ||
| 
						 | 
					d4db16665f | ||
| 
						 | 
					20b7a494f6 | ||
| 
						 | 
					fbdce3ad89 | ||
| 
						 | 
					4fc8807f02 | ||
| 
						 | 
					83075bfb5c | ||
| 
						 | 
					4074ec0425 | ||
| 
						 | 
					8e1694dd0f | ||
| 
						 | 
					911df18855 | ||
| 
						 | 
					6b049e93f8 | ||
| 
						 | 
					a335dcc379 | ||
| 
						 | 
					c6478c8a79 | ||
| 
						 | 
					cc9d40cb60 | ||
| 
						 | 
					0a6b7f9a1b | ||
| 
						 | 
					daa1fb9a7a | ||
| 
						 | 
					b7d543290b | ||
| 
						 | 
					ea852b60ac | ||
| 
						 | 
					ed341988ea | ||
| 
						 | 
					057b6c8e30 | ||
| 
						 | 
					44444fe071 | ||
| 
						 | 
					797330d6ab | ||
| 
						 | 
					a630d5b5f5 | ||
| 
						 | 
					eb3dc82b5d | ||
| 
						 | 
					34ed18d562 | ||
| 
						 | 
					1ce02ee313 | ||
| 
						 | 
					2a26a0188c | ||
| 
						 | 
					50cb05d1b1 | ||
| 
						 | 
					6e739ac453 | ||
| 
						 | 
					7aa2fd9f0e | ||
| 
						 | 
					8e254e1b03 | ||
| 
						 | 
					1ad9d717ff | ||
| 
						 | 
					104658e43a | ||
| 
						 | 
					e7e4b995bf | ||
| 
						 | 
					b35b54f2c2 | ||
| 
						 | 
					f80aeb1d1d | ||
| 
						 | 
					6a756ab3b6 | ||
| 
						 | 
					58a697bed1 | ||
| 
						 | 
					280960ac18 | ||
| 
						 | 
					0640ff13aa | ||
| 
						 | 
					545505691f | ||
| 
						 | 
					11fcf81321 | ||
| 
						 | 
					c565b37dc8 | ||
| 
						 | 
					3d18495270 | ||
| 
						 | 
					419e4e63e9 | ||
| 
						 | 
					724aa2bf65 | ||
| 
						 | 
					573fa8aeb3 | ||
| 
						 | 
					8a672e34c5 | ||
| 
						 | 
					bc49211dab | ||
| 
						 | 
					4ef9c3667e | ||
| 
						 | 
					6babe516ac | ||
| 
						 | 
					e0b258ef7e | ||
| 
						 | 
					ff0c3a89b1 | ||
| 
						 | 
					2511b81048 | ||
| 
						 | 
					6ffcd94edc | ||
| 
						 | 
					2fcf73c812 | ||
| 
						 | 
					dee0608af9 | ||
| 
						 | 
					d11860a383 | ||
| 
						 | 
					1c05115bf5 | ||
| 
						 | 
					d7e7382d0b | ||
| 
						 | 
					872388f6e3 | ||
| 
						 | 
					1215ef920b | ||
| 
						 | 
					d19d5a23ea | ||
| 
						 | 
					f49a779f1d | ||
| 
						 | 
					d8bf5b80e1 | ||
| 
						 | 
					69483b9353 | ||
| 
						 | 
					14e8548989 | ||
| 
						 | 
					4abd93b661 | ||
| 
						 | 
					5d925af76f | ||
| 
						 | 
					b999c6064a | ||
| 
						 | 
					94e3576978 | ||
| 
						 | 
					7a22406a2d | ||
| 
						 | 
					e60684494f | ||
| 
						 | 
					9db28ed779 | ||
| 
						 | 
					6fd8c5cee7 | ||
| 
						 | 
					787ec43266 | ||
| 
						 | 
					a4efc63bf2 | ||
| 
						 | 
					80a8f1437e | ||
| 
						 | 
					fcca94169d | ||
| 
						 | 
					d1924088e3 | ||
| 
						 | 
					fd31afe09c | ||
| 
						 | 
					7a763712c5 | ||
| 
						 | 
					7216be5da7 | ||
| 
						 | 
					711b0a291b | ||
| 
						 | 
					dfc96496c8 | ||
| 
						 | 
					2a1c5ef333 | ||
| 
						 | 
					9755209499 | ||
| 
						 | 
					0b26e537d4 | ||
| 
						 | 
					98c6233ec3 | ||
| 
						 | 
					f711706b1a | ||
| 
						 | 
					cee7789ab6 | ||
| 
						 | 
					8a06c4380d | ||
| 
						 | 
					72ecf7a288 | ||
| 
						 | 
					ef98c7502d | ||
| 
						 | 
					03d0e74b65 | ||
| 
						 | 
					5b8fdc0364 | ||
| 
						 | 
					593b4bd137 | ||
| 
						 | 
					267e12d058 | ||
| 
						 | 
					4a5e39b651 | ||
| 
						 | 
					ea24fa5b78 | ||
| 
						 | 
					bb2bb128f7 | ||
| 
						 | 
					94e8a856d7 | ||
| 
						 | 
					4c19fbf98e | ||
| 
						 | 
					60f8938bfa | ||
| 
						 | 
					55679662b5 | ||
| 
						 | 
					53df959e49 | ||
| 
						 | 
					8e6ef9966f | ||
| 
						 | 
					1d52fceafa | ||
| 
						 | 
					99186ed864 | ||
| 
						 | 
					383931d484 | ||
| 
						 | 
					0b49a54cb3 | ||
| 
						 | 
					705c0f1891 | ||
| 
						 | 
					544c3ffc95 | ||
| 
						 | 
					33f252a45d | ||
| 
						 | 
					f55d82a015 | ||
| 
						 | 
					8cf33fdef0 | ||
| 
						 | 
					f858d98811 | ||
| 
						 | 
					2a6165d440 | ||
| 
						 | 
					4586528c40 | ||
| 
						 | 
					23a07baa19 | ||
| 
						 | 
					f9040ca932 | ||
| 
						 | 
					4cea7f0237 | ||
| 
						 | 
					b1847d5e98 | ||
| 
						 | 
					9ce4d2e952 | ||
| 
						 | 
					247078e06d | ||
| 
						 | 
					a0cd72de28 | ||
| 
						 | 
					e467f569f0 | ||
| 
						 | 
					e31c7b7dfc | ||
| 
						 | 
					dc2e0c832b | ||
| 
						 | 
					7ddf51bb51 | ||
| 
						 | 
					8fb3856665 | ||
| 
						 | 
					183dd74f3e | ||
| 
						 | 
					4f29039b41 | ||
| 
						 | 
					102fcbec20 | ||
| 
						 | 
					d00e5212c7 | ||
| 
						 | 
					0e6bfb62cd | ||
| 
						 | 
					f576e8f635 | ||
| 
						 | 
					e6dc10a440 | ||
| 
						 | 
					aa930fb6b6 | ||
| 
						 | 
					f327ed87e9 | ||
| 
						 | 
					2de9be0589 | ||
| 
						 | 
					345cde8645 | ||
| 
						 | 
					cf152af9ae | ||
| 
						 | 
					d6333dcfd9 | ||
| 
						 | 
					0121f799f0 | ||
| 
						 | 
					82c39580df | ||
| 
						 | 
					53a578a46f | ||
| 
						 | 
					62612ef80b | ||
| 
						 | 
					61ac874c4c | ||
| 
						 | 
					976b200ff6 | ||
| 
						 | 
					852343b6d8 | ||
| 
						 | 
					c56af9d52b | ||
| 
						 | 
					05f18e2828 | ||
| 
						 | 
					72804caab2 | ||
| 
						 | 
					80cbe5c7c9 | ||
| 
						 | 
					21892d1236 | ||
| 
						 | 
					13824624f8 | ||
| 
						 | 
					0fd72ecbab | ||
| 
						 | 
					f848cb1546 | ||
| 
						 | 
					633854081a | ||
| 
						 | 
					4fed9a581b | ||
| 
						 | 
					e9c1202aaa | ||
| 
						 | 
					0a7ae279d0 | ||
| 
						 | 
					0de2696543 | ||
| 
						 | 
					a7dc239b71 | ||
| 
						 | 
					fe0e6990f5 | ||
| 
						 | 
					5ba65e92d9 | ||
| 
						 | 
					a1452b52c9 | ||
| 
						 | 
					dd2aa23a5f | ||
| 
						 | 
					0e0359ba7d | ||
| 
						 | 
					93b1b7aded | ||
| 
						 | 
					9472dc6a53 | ||
| 
						 | 
					67b681854e | ||
| 
						 | 
					7b5990833e | ||
| 
						 | 
					b6d5d04589 | ||
| 
						 | 
					fdfbb3e944 | ||
| 
						 | 
					faa7a3e37f | ||
| 
						 | 
					23748b82bb | ||
| 
						 | 
					bccb6f578a | ||
| 
						 | 
					de8a5d6e9e | ||
| 
						 | 
					a8eb3f7961 | ||
| 
						 | 
					2dc85f5a42 | ||
| 
						 | 
					82518b351d | ||
| 
						 | 
					68f34a1683 | ||
| 
						 | 
					bc6b72a422 | ||
| 
						 | 
					599e28e1cb | ||
| 
						 | 
					ee6b2ba6c6 | ||
| 
						 | 
					0877b3e2af | ||
| 
						 | 
					d1edb1e32a | ||
| 
						 | 
					d1e6b8dd10 | ||
| 
						 | 
					b32fc3bfdd | ||
| 
						 | 
					1e24417db0 | ||
| 
						 | 
					fb9387ecc5 | ||
| 
						 | 
					6c5f4cdb70 | ||
| 
						 | 
					aabacb7454 | ||
| 
						 | 
					b5da84479e | ||
| 
						 | 
					88d9361050 | ||
| 
						 | 
					1d90388ffc | ||
| 
						 | 
					b3c43ce31f | ||
| 
						 | 
					6d9d22d422 | ||
| 
						 | 
					86be1f56d0 | ||
| 
						 | 
					a0c81ffd7a | ||
| 
						 | 
					ec1dc42e58 | ||
| 
						 | 
					866eaed73d | ||
| 
						 | 
					a18374e1ad | ||
| 
						 | 
					f7afcb3b24 | ||
| 
						 | 
					3adcae783c | ||
| 
						 | 
					73b40dd2e7 | ||
| 
						 | 
					1e12614f9a | ||
| 
						 | 
					aeaa7c699a | ||
| 
						 | 
					f1c56b7254 | ||
| 
						 | 
					e72e0d0646 | ||
| 
						 | 
					5719d334aa | ||
| 
						 | 
					bcb6b85333 | ||
| 
						 | 
					5d765413ef | ||
| 
						 | 
					efb2e5e7a8 | ||
| 
						 | 
					5d5e346199 | ||
| 
						 | 
					08a74890da | ||
| 
						 | 
					0545b9c7f2 | ||
| 
						 | 
					bbf7d32676 | ||
| 
						 | 
					e83f4ae974 | ||
| 
						 | 
					9b0d01e03f | ||
| 
						 | 
					eae0d90a1e | ||
| 
						 | 
					90c09a7650 | ||
| 
						 | 
					aecf080211 | ||
| 
						 | 
					8517420356 | ||
| 
						 | 
					376be1f009 | ||
| 
						 | 
					0021e76649 | ||
| 
						 | 
					d440c4bc43 | ||
| 
						 | 
					50840b2105 | ||
| 
						 | 
					7502c6b6c0 | ||
| 
						 | 
					919c32f0cc | ||
| 
						 | 
					a28c951272 | ||
| 
						 | 
					13d7c5a9a9 | ||
| 
						 | 
					5f1383344d | ||
| 
						 | 
					48f43d3eb1 | ||
| 
						 | 
					4ac2141307 | ||
| 
						 | 
					719d8cac97 | ||
| 
						 | 
					99cbe53a8e | ||
| 
						 | 
					a36af1bfac | ||
| 
						 | 
					8b6aa319bf | ||
| 
						 | 
					a16d321e1a | ||
| 
						 | 
					74e70278e2 | ||
| 
						 | 
					1332e24a2c | ||
| 
						 | 
					5ab78ec461 | ||
| 
						 | 
					ce701d3c31 | ||
| 
						 | 
					5fca1be44d | ||
| 
						 | 
					0bd4c333bd | ||
| 
						 | 
					c6ed880732 | ||
| 
						 | 
					da0f3c6cce | ||
| 
						 | 
					e5d12d346a | ||
| 
						 | 
					478e2e726b | ||
| 
						 | 
					dbdac3707b | ||
| 
						 | 
					bd89a88e34 | ||
| 
						 | 
					d322d83745 | ||
| 
						 | 
					463a581ab9 | ||
| 
						 | 
					eae4bd222a | ||
| 
						 | 
					a7bb7fc14d | ||
| 
						 | 
					c047aa47eb | ||
| 
						 | 
					61bca56316 | ||
| 
						 | 
					9a37323eb8 | ||
| 
						 | 
					99a54369bf | ||
| 
						 | 
					f7533dfc5c | ||
| 
						 | 
					ee7d95272d | ||
| 
						 | 
					2b9b1d12e6 | ||
| 
						 | 
					2cbb5c7d8e | ||
| 
						 | 
					9686c7babe | ||
| 
						 | 
					66bd4c96c4 | ||
| 
						 | 
					dc47faa4b6 | ||
| 
						 | 
					55ee0b116d | ||
| 
						 | 
					c6957c08bc | ||
| 
						 | 
					8fe6a323d8 | ||
| 
						 | 
					8e51590c32 | ||
| 
						 | 
					ae066d5627 | ||
| 
						 | 
					6760279916 | ||
| 
						 | 
					3c208050b0 | ||
| 
						 | 
					bbc7c9fb37 | ||
| 
						 | 
					e1c3862586 | ||
| 
						 | 
					c24b7cb7bd | ||
| 
						 | 
					c91e16549d | ||
| 
						 | 
					6e70aca458 | ||
| 
						 | 
					d9ffd0ac8e | ||
| 
						 | 
					4641f73d19 | ||
| 
						 | 
					9f0051c21f | ||
| 
						 | 
					0331cb09e8 | ||
| 
						 | 
					2f8946f86c | ||
| 
						 | 
					88a3df4008 | ||
| 
						 | 
					0adf514bd6 | ||
| 
						 | 
					a1b5a2abcb | ||
| 
						 | 
					068c62c6fe | ||
| 
						 | 
					0e9f14f969 | ||
| 
						 | 
					78315fd388 | ||
| 
						 | 
					0ab69002df | ||
| 
						 | 
					1eec1239ec | ||
| 
						 | 
					57f4067fbf | ||
| 
						 | 
					f4a9221232 | ||
| 
						 | 
					3d4a75148d | ||
| 
						 | 
					c2c5bd844d | ||
| 
						 | 
					98a2f23024 | ||
| 
						 | 
					c955897d1b | ||
| 
						 | 
					9624efa21e | ||
| 
						 | 
					831638210d | ||
| 
						 | 
					cfdb0925ce | ||
| 
						 | 
					83db3eddd9 | ||
| 
						 | 
					cc2c5a544e | ||
| 
						 | 
					8fba8c2800 | ||
| 
						 | 
					51d1da8460 | ||
| 
						 | 
					2f1257056d | ||
| 
						 | 
					2f8f6967bf | ||
| 
						 | 
					246527e618 | ||
| 
						 | 
					3857cc9c83 | ||
| 
						 | 
					a59a8c563e | ||
| 
						 | 
					856829bcbb | ||
| 
						 | 
					dd2b931f61 | ||
| 
						 | 
					39beccbbb0 | ||
| 
						 | 
					ff626b428f | ||
| 
						 | 
					3915e1f012 | ||
| 
						 | 
					7b460b6224 | ||
| 
						 | 
					8fb8e79730 | ||
| 
						 | 
					79bbc475f4 | ||
| 
						 | 
					cef023283b | ||
| 
						 | 
					d4fda79ada | ||
| 
						 | 
					ff0bdcf4cd | ||
| 
						 | 
					bfbc313144 | ||
| 
						 | 
					31f2376f15 | ||
| 
						 | 
					f76ecb6604 | ||
| 
						 | 
					298cc58433 | ||
| 
						 | 
					825c0593e1 | ||
| 
						 | 
					87ed1dc3e3 | ||
| 
						 | 
					67e9db021c | ||
| 
						 | 
					3922950951 | ||
| 
						 | 
					9c4aa0ba53 | ||
| 
						 | 
					f5f1651b31 | ||
| 
						 | 
					32f4e4ca13 | ||
| 
						 | 
					962e0c4c33 | ||
| 
						 | 
					2c01bc5795 | ||
| 
						 | 
					0651f7cb3c | ||
| 
						 | 
					01ac59ce2a | ||
| 
						 | 
					c1fd597757 | ||
| 
						 | 
					e79e244eee | ||
| 
						 | 
					68ecc08111 | ||
| 
						 | 
					3b5fbc359f | ||
| 
						 | 
					583e5ea47f | ||
| 
						 | 
					7b647c3fae | ||
| 
						 | 
					a8b76c617c | ||
| 
						 | 
					1bd8985dff | ||
| 
						 | 
					25b5a6c4ae | 
@@ -329,6 +329,7 @@ esphome/components/opentherm/* @olegtarasov
 | 
			
		||||
esphome/components/openthread/* @mrene
 | 
			
		||||
esphome/components/opt3001/* @ccutrer
 | 
			
		||||
esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/ota_base/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/pca6416a/* @Mat931
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,15 @@ from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ANALOG,
 | 
			
		||||
    CONF_INPUT,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
@@ -229,3 +236,20 @@ def validate_adc_pin(value):
 | 
			
		||||
        )(value)
 | 
			
		||||
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "adc_sensor_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "adc_sensor_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VARIABLES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
@@ -313,3 +313,14 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
 | 
			
		||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
 | 
			
		||||
async def api_connected_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def FILTER_SOURCE_FILES() -> list[str]:
 | 
			
		||||
    """Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
 | 
			
		||||
    # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
 | 
			
		||||
    # This is a particularly large file that still needs to be opened and read
 | 
			
		||||
    # all the way to the end even when ifdef'd out
 | 
			
		||||
    if "HAS_PROTO_MESSAGE_DUMP" not in CORE.defines:
 | 
			
		||||
        return ["api_pb2_dump.cpp"]
 | 
			
		||||
 | 
			
		||||
    return []
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,19 @@ static const char *const TAG = "api.connection";
 | 
			
		||||
static const int CAMERA_STOP_STREAM = 5000;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object
 | 
			
		||||
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
 | 
			
		||||
  entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
 | 
			
		||||
  if (entity_var == nullptr) \
 | 
			
		||||
    return; \
 | 
			
		||||
  auto call = entity_var->make_call();
 | 
			
		||||
 | 
			
		||||
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found
 | 
			
		||||
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
 | 
			
		||||
  entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
 | 
			
		||||
  if (entity_var == nullptr) \
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
 | 
			
		||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
 | 
			
		||||
@@ -361,11 +374,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
  cover::Cover *cover = App.get_cover_by_key(msg.key);
 | 
			
		||||
  if (cover == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = cover->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
 | 
			
		||||
  if (msg.has_legacy_command) {
 | 
			
		||||
    switch (msg.legacy_command) {
 | 
			
		||||
      case enums::LEGACY_COVER_COMMAND_OPEN:
 | 
			
		||||
@@ -427,11 +436,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  fan::Fan *fan = App.get_fan_by_key(msg.key);
 | 
			
		||||
  if (fan == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = fan->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
 | 
			
		||||
  if (msg.has_state)
 | 
			
		||||
    call.set_state(msg.state);
 | 
			
		||||
  if (msg.has_oscillating)
 | 
			
		||||
@@ -504,11 +509,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::light_command(const LightCommandRequest &msg) {
 | 
			
		||||
  light::LightState *light = App.get_light_by_key(msg.key);
 | 
			
		||||
  if (light == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = light->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
 | 
			
		||||
  if (msg.has_state)
 | 
			
		||||
    call.set_state(msg.state);
 | 
			
		||||
  if (msg.has_brightness)
 | 
			
		||||
@@ -597,9 +598,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
 | 
			
		||||
  switch_::Switch *a_switch = App.get_switch_by_key(msg.key);
 | 
			
		||||
  if (a_switch == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
  ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
 | 
			
		||||
 | 
			
		||||
  if (msg.state) {
 | 
			
		||||
    a_switch->turn_on();
 | 
			
		||||
@@ -708,11 +707,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
 | 
			
		||||
  climate::Climate *climate = App.get_climate_by_key(msg.key);
 | 
			
		||||
  if (climate == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = climate->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
 | 
			
		||||
  if (msg.has_mode)
 | 
			
		||||
    call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
 | 
			
		||||
  if (msg.has_target_temperature)
 | 
			
		||||
@@ -767,11 +762,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::number_command(const NumberCommandRequest &msg) {
 | 
			
		||||
  number::Number *number = App.get_number_by_key(msg.key);
 | 
			
		||||
  if (number == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = number->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
 | 
			
		||||
  call.set_value(msg.state);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -801,11 +792,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::date_command(const DateCommandRequest &msg) {
 | 
			
		||||
  datetime::DateEntity *date = App.get_date_by_key(msg.key);
 | 
			
		||||
  if (date == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = date->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
 | 
			
		||||
  call.set_date(msg.year, msg.month, msg.day);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -835,11 +822,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::time_command(const TimeCommandRequest &msg) {
 | 
			
		||||
  datetime::TimeEntity *time = App.get_time_by_key(msg.key);
 | 
			
		||||
  if (time == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = time->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
 | 
			
		||||
  call.set_time(msg.hour, msg.minute, msg.second);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -871,11 +854,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
 | 
			
		||||
  datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
 | 
			
		||||
  if (datetime == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = datetime->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
 | 
			
		||||
  call.set_datetime(msg.epoch_seconds);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -909,11 +888,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::text_command(const TextCommandRequest &msg) {
 | 
			
		||||
  text::Text *text = App.get_text_by_key(msg.key);
 | 
			
		||||
  if (text == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = text->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
 | 
			
		||||
  call.set_value(msg.state);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -945,11 +920,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
 | 
			
		||||
  select::Select *select = App.get_select_by_key(msg.key);
 | 
			
		||||
  if (select == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = select->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
 | 
			
		||||
  call.set_option(msg.state);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
@@ -966,10 +937,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
 | 
			
		||||
  button::Button *button = App.get_button_by_key(msg.key);
 | 
			
		||||
  if (button == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  ENTITY_COMMAND_GET(button::Button, button, button)
 | 
			
		||||
  button->press();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1000,9 +968,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::lock_command(const LockCommandRequest &msg) {
 | 
			
		||||
  lock::Lock *a_lock = App.get_lock_by_key(msg.key);
 | 
			
		||||
  if (a_lock == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
  ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
 | 
			
		||||
 | 
			
		||||
  switch (msg.command) {
 | 
			
		||||
    case enums::LOCK_UNLOCK:
 | 
			
		||||
@@ -1045,11 +1011,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::valve_command(const ValveCommandRequest &msg) {
 | 
			
		||||
  valve::Valve *valve = App.get_valve_by_key(msg.key);
 | 
			
		||||
  if (valve == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = valve->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
 | 
			
		||||
  if (msg.has_position)
 | 
			
		||||
    call.set_position(msg.position);
 | 
			
		||||
  if (msg.stop)
 | 
			
		||||
@@ -1096,11 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
 | 
			
		||||
  if (media_player == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = media_player->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
 | 
			
		||||
  if (msg.has_command) {
 | 
			
		||||
    call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
 | 
			
		||||
  }
 | 
			
		||||
@@ -1346,11 +1304,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
 | 
			
		||||
                                  is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
 | 
			
		||||
  alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key);
 | 
			
		||||
  if (a_alarm_control_panel == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = a_alarm_control_panel->make_call();
 | 
			
		||||
  ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
 | 
			
		||||
  switch (msg.command) {
 | 
			
		||||
    case enums::ALARM_CONTROL_PANEL_DISARM:
 | 
			
		||||
      call.disarm();
 | 
			
		||||
@@ -1438,9 +1392,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
 | 
			
		||||
  return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::update_command(const UpdateCommandRequest &msg) {
 | 
			
		||||
  update::UpdateEntity *update = App.get_update_by_key(msg.key);
 | 
			
		||||
  if (update == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
  ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
 | 
			
		||||
 | 
			
		||||
  switch (msg.command) {
 | 
			
		||||
    case enums::UPDATE_COMMAND_UPDATE:
 | 
			
		||||
@@ -1459,12 +1411,11 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) {
 | 
			
		||||
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
 | 
			
		||||
  if (this->flags_.log_subscription < level)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  // Pre-calculate message size to avoid reallocations
 | 
			
		||||
  const size_t line_length = strlen(line);
 | 
			
		||||
  uint32_t msg_size = 0;
 | 
			
		||||
 | 
			
		||||
  // Add size for level field (field ID 1, varint type)
 | 
			
		||||
@@ -1473,14 +1424,14 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
 | 
			
		||||
 | 
			
		||||
  // Add size for string field (field ID 3, string type)
 | 
			
		||||
  // 1 byte for field tag + size of length varint + string length
 | 
			
		||||
  msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(line_length)) + line_length;
 | 
			
		||||
  msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(message_len)) + message_len;
 | 
			
		||||
 | 
			
		||||
  // Create a pre-sized buffer
 | 
			
		||||
  auto buffer = this->create_buffer(msg_size);
 | 
			
		||||
 | 
			
		||||
  // Encode the message (SubscribeLogsResponse)
 | 
			
		||||
  buffer.encode_uint32(1, static_cast<uint32_t>(level));  // LogLevel level = 1
 | 
			
		||||
  buffer.encode_string(3, line, line_length);             // string message = 3
 | 
			
		||||
  buffer.encode_string(3, line, message_len);             // string message = 3
 | 
			
		||||
 | 
			
		||||
  // SubscribeLogsResponse - 29
 | 
			
		||||
  return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool send_media_player_state(media_player::MediaPlayer *media_player);
 | 
			
		||||
  void media_player_command(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool try_send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
    if (!this->flags_.service_call_subscription)
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -225,6 +225,22 @@ APIError APIFrameHelper::init_common_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
 | 
			
		||||
  if (received == -1) {
 | 
			
		||||
    if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
  } else if (received == 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Connection closed");
 | 
			
		||||
    return APIError::CONNECTION_CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
@@ -327,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
    uint8_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
      }
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    } else if (received == 0) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    APIError err = handle_socket_read_result_(received);
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_len_ += static_cast<uint8_t>(received);
 | 
			
		||||
    if (static_cast<uint8_t>(received) != to_read) {
 | 
			
		||||
@@ -372,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    uint16_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
      }
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    } else if (received == 0) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    APIError err = handle_socket_read_result_(received);
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
@@ -855,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
 | 
			
		||||
    ssize_t received =
 | 
			
		||||
        this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
      }
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    } else if (received == 0) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    APIError err = handle_socket_read_result_(received);
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If this was the first read, validate the indicator byte
 | 
			
		||||
@@ -949,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
      }
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    } else if (received == 0) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    APIError err = handle_socket_read_result_(received);
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
 
 | 
			
		||||
@@ -176,6 +176,9 @@ class APIFrameHelper {
 | 
			
		||||
 | 
			
		||||
  // Common initialization for both plaintext and noise protocols
 | 
			
		||||
  APIError init_common_();
 | 
			
		||||
 | 
			
		||||
  // Helper method to handle socket read results
 | 
			
		||||
  APIError handle_socket_read_result_(ssize_t received);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -104,18 +104,19 @@ void APIServer::setup() {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (logger::global_logger != nullptr) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
 | 
			
		||||
      if (this->shutting_down_) {
 | 
			
		||||
        // Don't try to send logs during shutdown
 | 
			
		||||
        // as it could result in a recursion and
 | 
			
		||||
        // we would be filling a buffer we are trying to clear
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      for (auto &c : this->clients_) {
 | 
			
		||||
        if (!c->flags_.remove)
 | 
			
		||||
          c->try_send_log_message(level, tag, message);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    logger::global_logger->add_on_log_callback(
 | 
			
		||||
        [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
          if (this->shutting_down_) {
 | 
			
		||||
            // Don't try to send logs during shutdown
 | 
			
		||||
            // as it could result in a recursion and
 | 
			
		||||
            // we would be filling a buffer we are trying to clear
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          for (auto &c : this->clients_) {
 | 
			
		||||
            if (!c->flags_.remove)
 | 
			
		||||
              c->try_send_log_message(level, tag, message, message_len);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
@@ -260,180 +261,108 @@ bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
			
		||||
 | 
			
		||||
// Macro for entities without extra parameters
 | 
			
		||||
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
 | 
			
		||||
  void APIServer::on_##entity_name##_update(entity_type *obj) { \
 | 
			
		||||
    if (obj->is_internal()) \
 | 
			
		||||
      return; \
 | 
			
		||||
    for (auto &c : this->clients_) \
 | 
			
		||||
      c->send_##entity_name##_state(obj); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
// Macro for entities with extra parameters (but parameters not used in send)
 | 
			
		||||
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
 | 
			
		||||
  void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { \
 | 
			
		||||
    if (obj->is_internal()) \
 | 
			
		||||
      return; \
 | 
			
		||||
    for (auto &c : this->clients_) \
 | 
			
		||||
      c->send_##entity_name##_state(obj); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_binary_sensor_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
void APIServer::on_cover_update(cover::Cover *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_cover_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(cover::Cover, cover)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
void APIServer::on_fan_update(fan::Fan *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_fan_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(fan::Fan, fan)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
void APIServer::on_light_update(light::LightState *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_light_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(light::LightState, light)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_sensor_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_switch_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_text_sensor_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
void APIServer::on_climate_update(climate::Climate *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_climate_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(climate::Climate, climate)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_number_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
void APIServer::on_date_update(datetime::DateEntity *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_date_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(datetime::DateEntity, date)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
void APIServer::on_time_update(datetime::TimeEntity *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_time_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(datetime::TimeEntity, time)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_datetime_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
void APIServer::on_text_update(text::Text *obj, const std::string &state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_text_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_select_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
void APIServer::on_lock_update(lock::Lock *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_lock_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(lock::Lock, lock)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
void APIServer::on_valve_update(valve::Valve *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_valve_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(valve::Valve, valve)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_media_player_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
// Event is a special case - it's the only entity that passes extra parameters to the send method
 | 
			
		||||
void APIServer::on_event(event::Event *obj, const std::string &event_type) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_event(obj, event_type);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
void APIServer::on_update(update::UpdateEntity *obj) {
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_update_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(update::UpdateEntity, update)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_alarm_control_panel_state(obj);
 | 
			
		||||
}
 | 
			
		||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
 
 | 
			
		||||
@@ -52,11 +52,19 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static constexpr size_t FLUSH_BATCH_SIZE = 8;
 | 
			
		||||
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
 | 
			
		||||
  static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
 | 
			
		||||
  return batch_buffer;
 | 
			
		||||
}
 | 
			
		||||
// Batch size for BLE advertisements to maximize WiFi efficiency
 | 
			
		||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
 | 
			
		||||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
 | 
			
		||||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
 | 
			
		||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
 | 
			
		||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
// Global batch buffer to avoid guard variable (saves 8 bytes)
 | 
			
		||||
// This is initialized at program startup before any threads
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
 | 
			
		||||
 | 
			
		||||
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
 | 
			
		||||
 | 
			
		||||
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
 | 
			
		||||
  if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BLOCK,
 | 
			
		||||
@@ -7,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_FREE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LOOP_TIME,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
@@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "debug_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "debug_host.cpp": {PlatformFramework.HOST_NATIVE},
 | 
			
		||||
        "debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "debug_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEFAULT,
 | 
			
		||||
@@ -27,6 +28,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_WAKEUP_PIN,
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
WAKEUP_PINS = {
 | 
			
		||||
@@ -313,3 +315,14 @@ async def deep_sleep_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "deep_sleep_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								esphome/components/esp32/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/esp32/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esp_efuse.h"
 | 
			
		||||
#include "esp_efuse_table.h"
 | 
			
		||||
#include "esp_mac.h"
 | 
			
		||||
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/portmacro.h>
 | 
			
		||||
#include "esp_random.h"
 | 
			
		||||
#include "esp_system.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
uint32_t random_uint32() { return esp_random(); }
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  esp_fill_random(data, len);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
 | 
			
		||||
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
 | 
			
		||||
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
 | 
			
		||||
 | 
			
		||||
// only affects the executing core
 | 
			
		||||
// so should not be used as a mutex lock, only to get accurate timing
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
 | 
			
		||||
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
 | 
			
		||||
  // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
 | 
			
		||||
  // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
 | 
			
		||||
  if (has_custom_mac_address()) {
 | 
			
		||||
    esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
 | 
			
		||||
  } else {
 | 
			
		||||
    esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  if (has_custom_mac_address()) {
 | 
			
		||||
    esp_efuse_mac_get_custom(mac);
 | 
			
		||||
  } else {
 | 
			
		||||
    esp_efuse_mac_get_default(mac);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
 | 
			
		||||
 | 
			
		||||
bool has_custom_mac_address() {
 | 
			
		||||
#if !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
 | 
			
		||||
  uint8_t mac[6];
 | 
			
		||||
  // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
 | 
			
		||||
#ifndef USE_ESP32_VARIANT_ESP32
 | 
			
		||||
  return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
 | 
			
		||||
#else
 | 
			
		||||
  return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
 | 
			
		||||
#endif
 | 
			
		||||
#else
 | 
			
		||||
  return false;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -25,10 +25,15 @@ namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
 | 
			
		||||
// Maximum number of BLE scan results to buffer
 | 
			
		||||
// Sized to handle bursts of advertisements while allowing for processing delays
 | 
			
		||||
// With 16 advertisements per batch and some safety margin:
 | 
			
		||||
// - Without PSRAM: 24 entries (1.5× batch size)
 | 
			
		||||
// - With PSRAM: 36 entries (2.25× batch size)
 | 
			
		||||
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
 | 
			
		||||
#ifdef USE_PSRAM
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32;
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
 | 
			
		||||
#else
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20;
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								esphome/components/esp8266/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/esp8266/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
 | 
			
		||||
#include <osapi.h>
 | 
			
		||||
#include <user_interface.h>
 | 
			
		||||
// for xt_rsil()/xt_wsr_ps()
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
uint32_t random_uint32() { return os_random(); }
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
 | 
			
		||||
 | 
			
		||||
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
 | 
			
		||||
Mutex::Mutex() {}
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() {}
 | 
			
		||||
bool Mutex::try_lock() { return true; }
 | 
			
		||||
void Mutex::unlock() {}
 | 
			
		||||
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
 | 
			
		||||
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  wifi_get_macaddr(STATION_IF, mac);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP8266
 | 
			
		||||
							
								
								
									
										57
									
								
								esphome/components/host/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								esphome/components/host/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOST
 | 
			
		||||
 | 
			
		||||
#ifndef _WIN32
 | 
			
		||||
#include <net/if.h>
 | 
			
		||||
#include <netinet/in.h>
 | 
			
		||||
#include <sys/ioctl.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <random>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "helpers.host";
 | 
			
		||||
 | 
			
		||||
uint32_t random_uint32() {
 | 
			
		||||
  std::random_device dev;
 | 
			
		||||
  std::mt19937 rng(dev());
 | 
			
		||||
  std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max());
 | 
			
		||||
  return dist(rng);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  FILE *fp = fopen("/dev/urandom", "r");
 | 
			
		||||
  if (fp == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
 | 
			
		||||
    exit(1);
 | 
			
		||||
  }
 | 
			
		||||
  size_t read = fread(data, 1, len, fp);
 | 
			
		||||
  if (read != len) {
 | 
			
		||||
    ESP_LOGW(TAG, "Not enough data from /dev/urandom");
 | 
			
		||||
    exit(1);
 | 
			
		||||
  }
 | 
			
		||||
  fclose(fp);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Host platform uses std::mutex for proper thread synchronization
 | 
			
		||||
Mutex::Mutex() { handle_ = new std::mutex(); }
 | 
			
		||||
Mutex::~Mutex() { delete static_cast<std::mutex *>(handle_); }
 | 
			
		||||
void Mutex::lock() { static_cast<std::mutex *>(handle_)->lock(); }
 | 
			
		||||
bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock(); }
 | 
			
		||||
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
 | 
			
		||||
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
 | 
			
		||||
  memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_HOST
 | 
			
		||||
@@ -2,6 +2,7 @@ from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32
 | 
			
		||||
from esphome.components.const import CONF_REQUEST_HEADERS
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ESP8266_DISABLE_SSL_SUPPORT,
 | 
			
		||||
@@ -13,6 +14,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
    CONF_WATCHDOG_TIMEOUT,
 | 
			
		||||
    PLATFORM_HOST,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
    __version__,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, Lambda
 | 
			
		||||
@@ -319,3 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "http_request_host.cpp": {PlatformFramework.HOST_NATIVE},
 | 
			
		||||
        "http_request_arduino.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.RP2040_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "http_request_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import logging
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ADDRESS,
 | 
			
		||||
@@ -18,6 +19,7 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PLATFORM_RP2040,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
@@ -205,3 +207,18 @@ def final_validate_device_schema(
 | 
			
		||||
        {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)},
 | 
			
		||||
        extra=cv.ALLOW_EXTRA,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "i2c_bus_arduino.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.RP2040_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,13 @@ from esphome.const import (
 | 
			
		||||
 | 
			
		||||
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
 | 
			
		||||
 | 
			
		||||
ResetButton = ld2450_ns.class_("ResetButton", button.Button)
 | 
			
		||||
FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
 | 
			
		||||
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = {
 | 
			
		||||
    cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
 | 
			
		||||
    cv.Optional(CONF_FACTORY_RESET): button.button_schema(
 | 
			
		||||
        ResetButton,
 | 
			
		||||
        FactoryResetButton,
 | 
			
		||||
        device_class=DEVICE_CLASS_RESTART,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_RESTART_ALERT,
 | 
			
		||||
@@ -38,7 +38,7 @@ async def to_code(config):
 | 
			
		||||
    if factory_reset_config := config.get(CONF_FACTORY_RESET):
 | 
			
		||||
        b = await button.new_button(factory_reset_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2450_ID])
 | 
			
		||||
        cg.add(ld2450_component.set_reset_button(b))
 | 
			
		||||
        cg.add(ld2450_component.set_factory_reset_button(b))
 | 
			
		||||
    if restart_config := config.get(CONF_RESTART):
 | 
			
		||||
        b = await button.new_button(restart_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2450_ID])
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
#include "factory_reset_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -6,9 +6,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
class ResetButton : public button::Button, public Parented<LD2450Component> {
 | 
			
		||||
class FactoryResetButton : public button::Button, public Parented<LD2450Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ResetButton() = default;
 | 
			
		||||
  FactoryResetButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#include "reset_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
void ResetButton::press_action() { this->parent_->factory_reset(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -18,11 +18,10 @@ namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ld2450";
 | 
			
		||||
static const char *const NO_MAC = "08:05:04:03:02:01";
 | 
			
		||||
static const char *const UNKNOWN_MAC = "unknown";
 | 
			
		||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
 | 
			
		||||
 | 
			
		||||
enum BaudRateStructure : uint8_t {
 | 
			
		||||
enum BaudRate : uint8_t {
 | 
			
		||||
  BAUD_RATE_9600 = 1,
 | 
			
		||||
  BAUD_RATE_19200 = 2,
 | 
			
		||||
  BAUD_RATE_38400 = 3,
 | 
			
		||||
@@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t {
 | 
			
		||||
  BAUD_RATE_460800 = 8
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Zone type struct
 | 
			
		||||
enum ZoneTypeStructure : uint8_t {
 | 
			
		||||
enum ZoneType : uint8_t {
 | 
			
		||||
  ZONE_DISABLED = 0,
 | 
			
		||||
  ZONE_DETECTION = 1,
 | 
			
		||||
  ZONE_FILTER = 2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum PeriodicDataStructure : uint8_t {
 | 
			
		||||
enum PeriodicData : uint8_t {
 | 
			
		||||
  TARGET_X = 4,
 | 
			
		||||
  TARGET_Y = 6,
 | 
			
		||||
  TARGET_SPEED = 8,
 | 
			
		||||
@@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum PeriodicDataValue : uint8_t {
 | 
			
		||||
  HEAD = 0xAA,
 | 
			
		||||
  END = 0x55,
 | 
			
		||||
  HEADER = 0xAA,
 | 
			
		||||
  FOOTER = 0x55,
 | 
			
		||||
  CHECK = 0x00,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum AckDataStructure : uint8_t {
 | 
			
		||||
enum AckData : uint8_t {
 | 
			
		||||
  COMMAND = 6,
 | 
			
		||||
  COMMAND_STATUS = 7,
 | 
			
		||||
};
 | 
			
		||||
@@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t {
 | 
			
		||||
// Memory-efficient lookup tables
 | 
			
		||||
struct StringToUint8 {
 | 
			
		||||
  const char *str;
 | 
			
		||||
  uint8_t value;
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Uint8ToString {
 | 
			
		||||
  uint8_t value;
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
  const char *str;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
 | 
			
		||||
    {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
 | 
			
		||||
    {DIRECTION_APPROACHING, "Approaching"},
 | 
			
		||||
    {DIRECTION_MOVING_AWAY, "Moving away"},
 | 
			
		||||
    {DIRECTION_STATIONARY, "Stationary"},
 | 
			
		||||
    {DIRECTION_NA, "NA"},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
 | 
			
		||||
    {ZONE_DISABLED, "Disabled"},
 | 
			
		||||
    {ZONE_DETECTION, "Detection"},
 | 
			
		||||
@@ -104,28 +109,35 @@ template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t v
 | 
			
		||||
  return "";  // Not found
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LD2450 serial command header & footer
 | 
			
		||||
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
 | 
			
		||||
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
 | 
			
		||||
// LD2450 UART Serial Commands
 | 
			
		||||
static const uint8_t CMD_ENABLE_CONF = 0xFF;
 | 
			
		||||
static const uint8_t CMD_DISABLE_CONF = 0xFE;
 | 
			
		||||
static const uint8_t CMD_VERSION = 0xA0;
 | 
			
		||||
static const uint8_t CMD_MAC = 0xA5;
 | 
			
		||||
static const uint8_t CMD_RESET = 0xA2;
 | 
			
		||||
static const uint8_t CMD_RESTART = 0xA3;
 | 
			
		||||
static const uint8_t CMD_BLUETOOTH = 0xA4;
 | 
			
		||||
static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
 | 
			
		||||
static const uint8_t CMD_MULTI_TARGET_MODE = 0x90;
 | 
			
		||||
static const uint8_t CMD_QUERY_TARGET_MODE = 0x91;
 | 
			
		||||
static const uint8_t CMD_SET_BAUD_RATE = 0xA1;
 | 
			
		||||
static const uint8_t CMD_QUERY_ZONE = 0xC1;
 | 
			
		||||
static const uint8_t CMD_SET_ZONE = 0xC2;
 | 
			
		||||
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
 | 
			
		||||
static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
 | 
			
		||||
static constexpr uint8_t CMD_RESET = 0xA2;
 | 
			
		||||
static constexpr uint8_t CMD_RESTART = 0xA3;
 | 
			
		||||
static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
 | 
			
		||||
static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
 | 
			
		||||
static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91;
 | 
			
		||||
static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_ZONE = 0xC1;
 | 
			
		||||
static constexpr uint8_t CMD_SET_ZONE = 0xC2;
 | 
			
		||||
// Header & Footer size
 | 
			
		||||
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
 | 
			
		||||
// Command Header & Footer
 | 
			
		||||
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
 | 
			
		||||
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
 | 
			
		||||
// Data Header & Footer
 | 
			
		||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00};
 | 
			
		||||
static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
 | 
			
		||||
// MAC address the module uses when Bluetooth is disabled
 | 
			
		||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
 | 
			
		||||
 | 
			
		||||
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
 | 
			
		||||
 | 
			
		||||
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
 | 
			
		||||
  for (int i = 0; i < 4; i++) {
 | 
			
		||||
  for (uint8_t i = 0; i < 4; i++) {
 | 
			
		||||
    uint16_t val = values[i] & 0xFFFF;
 | 
			
		||||
    bytes[i * 2] = val & 0xFF;             // Store low byte first (little-endian)
 | 
			
		||||
    bytes[i * 2 + 1] = (val >> 8) & 0xFF;  // Store high byte second
 | 
			
		||||
@@ -166,18 +178,13 @@ static inline float calculate_angle(float base, float hypotenuse) {
 | 
			
		||||
  return angle_degrees;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static inline std::string get_direction(int16_t speed) {
 | 
			
		||||
  static const char *const APPROACHING = "Approaching";
 | 
			
		||||
  static const char *const MOVING_AWAY = "Moving away";
 | 
			
		||||
  static const char *const STATIONARY = "Stationary";
 | 
			
		||||
 | 
			
		||||
  if (speed > 0) {
 | 
			
		||||
    return MOVING_AWAY;
 | 
			
		||||
static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
 | 
			
		||||
  for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
 | 
			
		||||
    if (header_footer[i] != buffer[i]) {
 | 
			
		||||
      return false;  // Mismatch in header/footer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (speed < 0) {
 | 
			
		||||
    return APPROACHING;
 | 
			
		||||
  }
 | 
			
		||||
  return STATIONARY;
 | 
			
		||||
  return true;  // Valid header/footer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2450Component::setup() {
 | 
			
		||||
@@ -192,84 +199,93 @@ void LD2450Component::setup() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2450Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "LD2450:");
 | 
			
		||||
  std::string mac_str =
 | 
			
		||||
      mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
 | 
			
		||||
  std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
 | 
			
		||||
                                    this->version_[4], this->version_[3], this->version_[2]);
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "LD2450:\n"
 | 
			
		||||
                "  Firmware version: %s\n"
 | 
			
		||||
                "  MAC address: %s\n"
 | 
			
		||||
                "  Throttle: %u ms",
 | 
			
		||||
                version.c_str(), mac_str.c_str(), this->throttle_);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_);
 | 
			
		||||
  LOG_SWITCH("  ", "MultiTargetSwitch", this->multi_target_switch_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  LOG_BUTTON("  ", "ResetButton", this->reset_button_);
 | 
			
		||||
  LOG_BUTTON("  ", "RestartButton", this->restart_button_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Binary Sensors:");
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  LOG_SENSOR("  ", "TargetCountSensor", this->target_count_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "StillTargetCountSensor", this->still_target_count_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "MovingTargetCountSensor", this->moving_target_count_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Sensors:");
 | 
			
		||||
  LOG_SENSOR("  ", "MovingTargetCount", this->moving_target_count_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "StillTargetCount", this->still_target_count_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TargetCount", this->target_count_sensor_);
 | 
			
		||||
  for (sensor::Sensor *s : this->move_x_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetXSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "TargetX", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_y_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetYSensor", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_speed_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetSpeedSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "TargetY", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_angle_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetAngleSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "TargetAngle", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_distance_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetDistanceSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "TargetDistance", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_resolution_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthTargetResolutionSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "TargetResolution", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->move_speed_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "TargetSpeed", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->zone_target_count_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthZoneTargetCountSensor", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthZoneStillTargetCountSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "ZoneTargetCount", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "NthZoneMovingTargetCountSensor", s);
 | 
			
		||||
    LOG_SENSOR("  ", "ZoneMovingTargetCount", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
 | 
			
		||||
    LOG_SENSOR("  ", "ZoneStillTargetCount", s);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_);
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Text Sensors:");
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_);
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Mac", this->mac_text_sensor_);
 | 
			
		||||
  for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
 | 
			
		||||
    LOG_TEXT_SENSOR("  ", "NthDirectionTextSensor", s);
 | 
			
		||||
    LOG_TEXT_SENSOR("  ", "Direction", s);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Numbers:");
 | 
			
		||||
  LOG_NUMBER("  ", "PresenceTimeout", this->presence_timeout_number_);
 | 
			
		||||
  for (auto n : this->zone_numbers_) {
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneX1Number", n.x1);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneY1Number", n.y1);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneX2Number", n.x2);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneY2Number", n.y2);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneX1", n.x1);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneY1", n.y1);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneX2", n.x2);
 | 
			
		||||
    LOG_NUMBER("  ", "ZoneY2", n.y2);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_);
 | 
			
		||||
  LOG_SELECT("  ", "ZoneTypeSelect", this->zone_type_select_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Selects:");
 | 
			
		||||
  LOG_SELECT("  ", "BaudRate", this->baud_rate_select_);
 | 
			
		||||
  LOG_SELECT("  ", "ZoneType", this->zone_type_select_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  LOG_NUMBER("  ", "PresenceTimeoutNumber", this->presence_timeout_number_);
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Switches:");
 | 
			
		||||
  LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_);
 | 
			
		||||
  LOG_SWITCH("  ", "MultiTarget", this->multi_target_switch_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Buttons:");
 | 
			
		||||
  LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_);
 | 
			
		||||
  LOG_BUTTON("  ", "Restart", this->restart_button_);
 | 
			
		||||
#endif
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "  Throttle: %ums\n"
 | 
			
		||||
                "  MAC Address: %s\n"
 | 
			
		||||
                "  Firmware version: %s",
 | 
			
		||||
                this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2450Component::loop() {
 | 
			
		||||
  while (this->available()) {
 | 
			
		||||
    this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH);
 | 
			
		||||
    this->readline_(this->read());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_
 | 
			
		||||
  this->zone_type_ = zone_type;
 | 
			
		||||
  int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
 | 
			
		||||
                             zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
 | 
			
		||||
  for (int i = 0; i < MAX_ZONES; i++) {
 | 
			
		||||
  for (uint8_t i = 0; i < MAX_ZONES; i++) {
 | 
			
		||||
    this->zone_config_[i].x1 = zone_parameters[i * 4];
 | 
			
		||||
    this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
 | 
			
		||||
    this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
 | 
			
		||||
@@ -318,15 +334,15 @@ void LD2450Component::send_set_zone_command_() {
 | 
			
		||||
  uint8_t cmd_value[26] = {};
 | 
			
		||||
  uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
 | 
			
		||||
  uint8_t area_config[24] = {};
 | 
			
		||||
  for (int i = 0; i < MAX_ZONES; i++) {
 | 
			
		||||
  for (uint8_t i = 0; i < MAX_ZONES; i++) {
 | 
			
		||||
    int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
 | 
			
		||||
                     this->zone_config_[i].y2};
 | 
			
		||||
    ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
 | 
			
		||||
  }
 | 
			
		||||
  std::memcpy(cmd_value, zone_type_bytes, 2);
 | 
			
		||||
  std::memcpy(cmd_value + 2, area_config, 24);
 | 
			
		||||
  std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
 | 
			
		||||
  std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(CMD_SET_ZONE, cmd_value, 26);
 | 
			
		||||
  this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extract, store and publish zone details LD2450 buffer
 | 
			
		||||
void LD2450Component::process_zone_(uint8_t *buffer) {
 | 
			
		||||
void LD2450Component::process_zone_() {
 | 
			
		||||
  uint8_t index, start;
 | 
			
		||||
  for (index = 0; index < MAX_ZONES; index++) {
 | 
			
		||||
    start = 12 + index * 8;
 | 
			
		||||
    this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start);
 | 
			
		||||
    this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2);
 | 
			
		||||
    this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
 | 
			
		||||
    this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
 | 
			
		||||
    this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
 | 
			
		||||
    this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
 | 
			
		||||
    this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
 | 
			
		||||
    this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
    // only one null check as all coordinates are required for a single zone
 | 
			
		||||
    if (this->zone_numbers_[index].x1 != nullptr) {
 | 
			
		||||
@@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() {
 | 
			
		||||
 | 
			
		||||
// Send command with values to LD2450
 | 
			
		||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
 | 
			
		||||
  ESP_LOGV(TAG, "Sending command %02X", command);
 | 
			
		||||
  // frame header
 | 
			
		||||
  this->write_array(CMD_FRAME_HEADER, 4);
 | 
			
		||||
  ESP_LOGV(TAG, "Sending COMMAND %02X", command);
 | 
			
		||||
  // frame header bytes
 | 
			
		||||
  this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
 | 
			
		||||
  // length bytes
 | 
			
		||||
  int len = 2;
 | 
			
		||||
  uint8_t len = 2;
 | 
			
		||||
  if (command_value != nullptr) {
 | 
			
		||||
    len += command_value_len;
 | 
			
		||||
  }
 | 
			
		||||
  this->write_byte(lowbyte(len));
 | 
			
		||||
  this->write_byte(highbyte(len));
 | 
			
		||||
  // command
 | 
			
		||||
  this->write_byte(lowbyte(command));
 | 
			
		||||
  this->write_byte(highbyte(command));
 | 
			
		||||
  uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
 | 
			
		||||
  this->write_array(len_cmd, sizeof(len_cmd));
 | 
			
		||||
 | 
			
		||||
  // command value bytes
 | 
			
		||||
  if (command_value != nullptr) {
 | 
			
		||||
    for (int i = 0; i < command_value_len; i++) {
 | 
			
		||||
    for (uint8_t i = 0; i < command_value_len; i++) {
 | 
			
		||||
      this->write_byte(command_value[i]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // footer
 | 
			
		||||
  this->write_array(CMD_FRAME_END, 4);
 | 
			
		||||
  // frame footer bytes
 | 
			
		||||
  this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
 | 
			
		||||
  // FIXME to remove
 | 
			
		||||
  delay(50);  // NOLINT
 | 
			
		||||
}
 | 
			
		||||
@@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
 | 
			
		||||
// LD2450 Radar data message:
 | 
			
		||||
//  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
 | 
			
		||||
//   Header       Target 1                  Target 2                  Target 3                  End
 | 
			
		||||
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
void LD2450Component::handle_periodic_data_() {
 | 
			
		||||
  // Early throttle check - moved before any processing to save CPU cycles
 | 
			
		||||
  if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
 | 
			
		||||
    ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid message length");
 | 
			
		||||
  if (this->buffer_pos_ < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid length");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) {  // header
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid message header");
 | 
			
		||||
  if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
 | 
			
		||||
      this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
 | 
			
		||||
      this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid header/footer");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) {  // footer
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid message footer");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
 | 
			
		||||
  this->last_periodic_millis_ = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
  int16_t target_count = 0;
 | 
			
		||||
@@ -450,13 +461,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
  int16_t moving_target_count = 0;
 | 
			
		||||
  int16_t start = 0;
 | 
			
		||||
  int16_t val = 0;
 | 
			
		||||
  uint8_t index = 0;
 | 
			
		||||
  int16_t tx = 0;
 | 
			
		||||
  int16_t ty = 0;
 | 
			
		||||
  int16_t td = 0;
 | 
			
		||||
  int16_t ts = 0;
 | 
			
		||||
  int16_t angle = 0;
 | 
			
		||||
  std::string direction{};
 | 
			
		||||
  uint8_t index = 0;
 | 
			
		||||
  Direction direction{DIRECTION_UNDEFINED};
 | 
			
		||||
  bool is_moving = false;
 | 
			
		||||
 | 
			
		||||
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
 | 
			
		||||
@@ -468,7 +479,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
    is_moving = false;
 | 
			
		||||
    sensor::Sensor *sx = this->move_x_sensors_[index];
 | 
			
		||||
    if (sx != nullptr) {
 | 
			
		||||
      val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
 | 
			
		||||
      val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
 | 
			
		||||
      tx = val;
 | 
			
		||||
      if (this->cached_target_data_[index].x != val) {
 | 
			
		||||
        sx->publish_state(val);
 | 
			
		||||
@@ -479,7 +490,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
    start = TARGET_Y + index * 8;
 | 
			
		||||
    sensor::Sensor *sy = this->move_y_sensors_[index];
 | 
			
		||||
    if (sy != nullptr) {
 | 
			
		||||
      val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
 | 
			
		||||
      val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
 | 
			
		||||
      ty = val;
 | 
			
		||||
      if (this->cached_target_data_[index].y != val) {
 | 
			
		||||
        sy->publish_state(val);
 | 
			
		||||
@@ -490,7 +501,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
    start = TARGET_RESOLUTION + index * 8;
 | 
			
		||||
    sensor::Sensor *sr = this->move_resolution_sensors_[index];
 | 
			
		||||
    if (sr != nullptr) {
 | 
			
		||||
      val = (buffer[start + 1] << 8) | buffer[start];
 | 
			
		||||
      val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
 | 
			
		||||
      if (this->cached_target_data_[index].resolution != val) {
 | 
			
		||||
        sr->publish_state(val);
 | 
			
		||||
        this->cached_target_data_[index].resolution = val;
 | 
			
		||||
@@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
#endif
 | 
			
		||||
    // SPEED
 | 
			
		||||
    start = TARGET_SPEED + index * 8;
 | 
			
		||||
    val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
 | 
			
		||||
    val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
 | 
			
		||||
    ts = val;
 | 
			
		||||
    if (val) {
 | 
			
		||||
      is_moving = true;
 | 
			
		||||
@@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // ANGLE
 | 
			
		||||
    angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td));
 | 
			
		||||
    angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
 | 
			
		||||
    if (tx > 0) {
 | 
			
		||||
      angle = angle * -1;
 | 
			
		||||
    }
 | 
			
		||||
@@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
    // DIRECTION
 | 
			
		||||
    direction = get_direction(ts);
 | 
			
		||||
    if (td == 0) {
 | 
			
		||||
      direction = "NA";
 | 
			
		||||
      direction = DIRECTION_NA;
 | 
			
		||||
    } else if (ts > 0) {
 | 
			
		||||
      direction = DIRECTION_MOVING_AWAY;
 | 
			
		||||
    } else if (ts < 0) {
 | 
			
		||||
      direction = DIRECTION_APPROACHING;
 | 
			
		||||
    } else {
 | 
			
		||||
      direction = DIRECTION_STATIONARY;
 | 
			
		||||
    }
 | 
			
		||||
    text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
 | 
			
		||||
    if (tsd != nullptr) {
 | 
			
		||||
      if (this->cached_target_data_[index].direction != direction) {
 | 
			
		||||
        tsd->publish_state(direction);
 | 
			
		||||
        tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
 | 
			
		||||
        this->cached_target_data_[index].direction = direction;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
  ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
 | 
			
		||||
  if (len < 10) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid ack length");
 | 
			
		||||
bool LD2450Component::handle_ack_data_() {
 | 
			
		||||
  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
 | 
			
		||||
  if (this->buffer_pos_ < 10) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid length");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // frame header
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]);
 | 
			
		||||
  if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[COMMAND_STATUS] != 0x01) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid ack status");
 | 
			
		||||
  if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid status");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[8] || buffer[9]) {
 | 
			
		||||
    ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]);
 | 
			
		||||
  if (this->buffer_data_[8] || this->buffer_data_[9]) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (buffer[COMMAND]) {
 | 
			
		||||
    case lowbyte(CMD_ENABLE_CONF):
 | 
			
		||||
      ESP_LOGV(TAG, "Enable conf command");
 | 
			
		||||
  switch (this->buffer_data_[COMMAND]) {
 | 
			
		||||
    case CMD_ENABLE_CONF:
 | 
			
		||||
      ESP_LOGV(TAG, "Enable conf");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_DISABLE_CONF):
 | 
			
		||||
      ESP_LOGV(TAG, "Disable conf command");
 | 
			
		||||
 | 
			
		||||
    case CMD_DISABLE_CONF:
 | 
			
		||||
      ESP_LOGV(TAG, "Disabled conf");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SET_BAUD_RATE):
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change command");
 | 
			
		||||
 | 
			
		||||
    case CMD_SET_BAUD_RATE:
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_VERSION):
 | 
			
		||||
      this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
 | 
			
		||||
      ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_VERSION: {
 | 
			
		||||
      std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
 | 
			
		||||
      std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
 | 
			
		||||
                                        this->version_[4], this->version_[3], this->version_[2]);
 | 
			
		||||
      ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->version_text_sensor_ != nullptr) {
 | 
			
		||||
        this->version_text_sensor_->publish_state(this->version_);
 | 
			
		||||
        this->version_text_sensor_->publish_state(version);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_MAC):
 | 
			
		||||
      if (len < 20) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_MAC_ADDRESS: {
 | 
			
		||||
      if (this->buffer_pos_ < 20) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      this->mac_ = format_mac_address_pretty(&buffer[10]);
 | 
			
		||||
      ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
 | 
			
		||||
 | 
			
		||||
      this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
 | 
			
		||||
      if (this->bluetooth_on_) {
 | 
			
		||||
        std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      std::string mac_str =
 | 
			
		||||
          mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
 | 
			
		||||
      ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->mac_text_sensor_ != nullptr) {
 | 
			
		||||
        this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
 | 
			
		||||
        this->mac_text_sensor_->publish_state(mac_str);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      if (this->bluetooth_switch_ != nullptr) {
 | 
			
		||||
        this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
 | 
			
		||||
        this->bluetooth_switch_->publish_state(this->bluetooth_on_);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_BLUETOOTH):
 | 
			
		||||
      ESP_LOGV(TAG, "Bluetooth command");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_BLUETOOTH:
 | 
			
		||||
      ESP_LOGV(TAG, "Bluetooth");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SINGLE_TARGET_MODE):
 | 
			
		||||
      ESP_LOGV(TAG, "Single target conf command");
 | 
			
		||||
 | 
			
		||||
    case CMD_SINGLE_TARGET_MODE:
 | 
			
		||||
      ESP_LOGV(TAG, "Single target conf");
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      if (this->multi_target_switch_ != nullptr) {
 | 
			
		||||
        this->multi_target_switch_->publish_state(false);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_MULTI_TARGET_MODE):
 | 
			
		||||
      ESP_LOGV(TAG, "Multi target conf command");
 | 
			
		||||
 | 
			
		||||
    case CMD_MULTI_TARGET_MODE:
 | 
			
		||||
      ESP_LOGV(TAG, "Multi target conf");
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      if (this->multi_target_switch_ != nullptr) {
 | 
			
		||||
        this->multi_target_switch_->publish_state(true);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_QUERY_TARGET_MODE):
 | 
			
		||||
      ESP_LOGV(TAG, "Query target tracking mode command");
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_TARGET_MODE:
 | 
			
		||||
      ESP_LOGV(TAG, "Query target tracking mode");
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      if (this->multi_target_switch_ != nullptr) {
 | 
			
		||||
        this->multi_target_switch_->publish_state(buffer[10] == 0x02);
 | 
			
		||||
        this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_QUERY_ZONE):
 | 
			
		||||
      ESP_LOGV(TAG, "Query zone conf command");
 | 
			
		||||
      this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_ZONE:
 | 
			
		||||
      ESP_LOGV(TAG, "Query zone conf");
 | 
			
		||||
      this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
 | 
			
		||||
      this->publish_zone_type();
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->zone_type_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      if (buffer[10] == 0x00) {
 | 
			
		||||
      if (this->buffer_data_[10] == 0x00) {
 | 
			
		||||
        ESP_LOGV(TAG, "Zone: Disabled");
 | 
			
		||||
      }
 | 
			
		||||
      if (buffer[10] == 0x01) {
 | 
			
		||||
      if (this->buffer_data_[10] == 0x01) {
 | 
			
		||||
        ESP_LOGV(TAG, "Zone: Area detection");
 | 
			
		||||
      }
 | 
			
		||||
      if (buffer[10] == 0x02) {
 | 
			
		||||
      if (this->buffer_data_[10] == 0x02) {
 | 
			
		||||
        ESP_LOGV(TAG, "Zone: Area filter");
 | 
			
		||||
      }
 | 
			
		||||
      this->process_zone_(buffer);
 | 
			
		||||
      this->process_zone_();
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SET_ZONE):
 | 
			
		||||
      ESP_LOGV(TAG, "Set zone conf command");
 | 
			
		||||
 | 
			
		||||
    case CMD_SET_ZONE:
 | 
			
		||||
      ESP_LOGV(TAG, "Set zone conf");
 | 
			
		||||
      this->query_zone_info();
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
@@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read LD2450 buffer data
 | 
			
		||||
void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) {
 | 
			
		||||
void LD2450Component::readline_(int readch) {
 | 
			
		||||
  if (readch < 0) {
 | 
			
		||||
    return;
 | 
			
		||||
    return;  // No data available
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_pos_ < len - 1) {
 | 
			
		||||
    buffer[this->buffer_pos_++] = readch;
 | 
			
		||||
    buffer[this->buffer_pos_] = 0;
 | 
			
		||||
 | 
			
		||||
  if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
 | 
			
		||||
    this->buffer_data_[this->buffer_pos_++] = readch;
 | 
			
		||||
    this->buffer_data_[this->buffer_pos_] = 0;
 | 
			
		||||
  } else {
 | 
			
		||||
    // We should never get here, but just in case...
 | 
			
		||||
    ESP_LOGW(TAG, "Max command length exceeded; ignoring");
 | 
			
		||||
    this->buffer_pos_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_pos_ < 4) {
 | 
			
		||||
    return;
 | 
			
		||||
    return;  // Not enough data to process yet
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handle periodic radar data");
 | 
			
		||||
    this->handle_periodic_data_(buffer, this->buffer_pos_);
 | 
			
		||||
  if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
 | 
			
		||||
      this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
 | 
			
		||||
    this->handle_periodic_data_();
 | 
			
		||||
    this->buffer_pos_ = 0;  // Reset position index for next frame
 | 
			
		||||
  } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 &&
 | 
			
		||||
             buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handle command ack data");
 | 
			
		||||
    if (this->handle_ack_data_(buffer, this->buffer_pos_)) {
 | 
			
		||||
      this->buffer_pos_ = 0;  // Reset position index for next frame
 | 
			
		||||
  } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
 | 
			
		||||
    if (this->handle_ack_data_()) {
 | 
			
		||||
      this->buffer_pos_ = 0;  // Reset position index for next message
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGV(TAG, "Command ack data invalid");
 | 
			
		||||
      ESP_LOGV(TAG, "Ack Data incomplete");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set Config Mode - Pre-requisite sending commands
 | 
			
		||||
void LD2450Component::set_config_mode_(bool enable) {
 | 
			
		||||
  uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
 | 
			
		||||
  uint8_t cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
 | 
			
		||||
  const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
 | 
			
		||||
  const uint8_t cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set Bluetooth Enable/Disable
 | 
			
		||||
void LD2450Component::set_bluetooth(bool enable) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  uint8_t enable_cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  uint8_t disable_cmd_value[2] = {0x00, 0x00};
 | 
			
		||||
  this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
 | 
			
		||||
  const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
 | 
			
		||||
  this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set Baud rate
 | 
			
		||||
void LD2450Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -885,12 +925,12 @@ void LD2450Component::factory_reset() {
 | 
			
		||||
void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
// Get LD2450 firmware version
 | 
			
		||||
void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
 | 
			
		||||
void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
// Get LD2450 mac address
 | 
			
		||||
void LD2450Component::get_mac_() {
 | 
			
		||||
  uint8_t cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  this->send_command_(CMD_MAC, cmd_value, 2);
 | 
			
		||||
  this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Query for target tracking mode
 | 
			
		||||
 
 | 
			
		||||
@@ -38,10 +38,18 @@ namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
// Constants
 | 
			
		||||
static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Timeout to reset presense status 5 sec.
 | 
			
		||||
static const uint8_t MAX_LINE_LENGTH = 60;          // Max characters for serial buffer
 | 
			
		||||
static const uint8_t MAX_LINE_LENGTH = 41;          // Max characters for serial buffer
 | 
			
		||||
static const uint8_t MAX_TARGETS = 3;               // Max 3 Targets in LD2450
 | 
			
		||||
static const uint8_t MAX_ZONES = 3;                 // Max 3 Zones in LD2450
 | 
			
		||||
 | 
			
		||||
enum Direction : uint8_t {
 | 
			
		||||
  DIRECTION_APPROACHING = 0,
 | 
			
		||||
  DIRECTION_MOVING_AWAY = 1,
 | 
			
		||||
  DIRECTION_STATIONARY = 2,
 | 
			
		||||
  DIRECTION_NA = 3,
 | 
			
		||||
  DIRECTION_UNDEFINED = 4,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Target coordinate struct
 | 
			
		||||
struct Target {
 | 
			
		||||
  int16_t x;
 | 
			
		||||
@@ -67,19 +75,22 @@ struct ZoneOfNumbers {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  SUB_SENSOR(target_count)
 | 
			
		||||
  SUB_SENSOR(still_target_count)
 | 
			
		||||
  SUB_SENSOR(moving_target_count)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  SUB_BINARY_SENSOR(target)
 | 
			
		||||
  SUB_BINARY_SENSOR(moving_target)
 | 
			
		||||
  SUB_BINARY_SENSOR(still_target)
 | 
			
		||||
  SUB_BINARY_SENSOR(target)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  SUB_SENSOR(moving_target_count)
 | 
			
		||||
  SUB_SENSOR(still_target_count)
 | 
			
		||||
  SUB_SENSOR(target_count)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  SUB_TEXT_SENSOR(version)
 | 
			
		||||
  SUB_TEXT_SENSOR(mac)
 | 
			
		||||
  SUB_TEXT_SENSOR(version)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  SUB_NUMBER(presence_timeout)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  SUB_SELECT(baud_rate)
 | 
			
		||||
@@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  SUB_SWITCH(multi_target)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  SUB_BUTTON(reset)
 | 
			
		||||
  SUB_BUTTON(factory_reset)
 | 
			
		||||
  SUB_BUTTON(restart)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  SUB_NUMBER(presence_timeout)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
 protected:
 | 
			
		||||
  void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
 | 
			
		||||
  void set_config_mode_(bool enable);
 | 
			
		||||
  void handle_periodic_data_(uint8_t *buffer, uint8_t len);
 | 
			
		||||
  bool handle_ack_data_(uint8_t *buffer, uint8_t len);
 | 
			
		||||
  void process_zone_(uint8_t *buffer);
 | 
			
		||||
  void readline_(int readch, uint8_t *buffer, uint8_t len);
 | 
			
		||||
  void handle_periodic_data_();
 | 
			
		||||
  bool handle_ack_data_();
 | 
			
		||||
  void process_zone_();
 | 
			
		||||
  void readline_(int readch);
 | 
			
		||||
  void get_version_();
 | 
			
		||||
  void get_mac_();
 | 
			
		||||
  void query_target_tracking_mode_();
 | 
			
		||||
@@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  uint32_t moving_presence_millis_ = 0;
 | 
			
		||||
  uint16_t throttle_ = 0;
 | 
			
		||||
  uint16_t timeout_ = 5;
 | 
			
		||||
  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
 | 
			
		||||
  uint8_t buffer_data_[MAX_LINE_LENGTH];
 | 
			
		||||
  uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
 | 
			
		||||
  uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
 | 
			
		||||
  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
 | 
			
		||||
  uint8_t zone_type_ = 0;
 | 
			
		||||
  bool bluetooth_on_{false};
 | 
			
		||||
  Target target_info_[MAX_TARGETS];
 | 
			
		||||
  Zone zone_config_[MAX_ZONES];
 | 
			
		||||
  std::string version_{};
 | 
			
		||||
  std::string mac_{};
 | 
			
		||||
 | 
			
		||||
  // Change detection - cache previous values to avoid redundant publishes
 | 
			
		||||
  // All values are initialized to sentinel values that are outside the valid sensor ranges
 | 
			
		||||
@@ -176,8 +185,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
    int16_t speed = std::numeric_limits<int16_t>::min();         // -32768, outside practical sensor range
 | 
			
		||||
    uint16_t resolution = std::numeric_limits<uint16_t>::max();  // 65535, unlikely resolution value
 | 
			
		||||
    uint16_t distance = std::numeric_limits<uint16_t>::max();    // 65535, outside range of 0 to ~8990
 | 
			
		||||
    Direction direction = DIRECTION_UNDEFINED;                   // Undefined, will differ from any real direction
 | 
			
		||||
    float angle = NAN;                                           // NAN, safe sentinel for floats
 | 
			
		||||
    std::string direction = "";                                  // Empty string, will differ from any real direction
 | 
			
		||||
  } cached_target_data_[MAX_TARGETS];
 | 
			
		||||
 | 
			
		||||
  struct CachedZoneData {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								esphome/components/libretiny/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/libretiny/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIBRETINY
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
#include <WiFi.h>  // for macAddress()
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
uint32_t random_uint32() { return rand(); }
 | 
			
		||||
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  lt_rand_bytes(data, len);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
 | 
			
		||||
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
 | 
			
		||||
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
 | 
			
		||||
 | 
			
		||||
// only affects the executing core
 | 
			
		||||
// so should not be used as a mutex lock, only to get accurate timing
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
 | 
			
		||||
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  WiFi.macAddress(mac);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_LIBRETINY
 | 
			
		||||
@@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component {
 | 
			
		||||
  }
 | 
			
		||||
  virtual ESPColorView get_view_internal(int32_t index) const = 0;
 | 
			
		||||
 | 
			
		||||
  bool effect_active_{false};
 | 
			
		||||
  ESPColorCorrection correction_{};
 | 
			
		||||
  LightState *state_parent_{nullptr};
 | 
			
		||||
#ifdef USE_POWER_SUPPLY
 | 
			
		||||
  power_supply::PowerSupplyRequester power_;
 | 
			
		||||
#endif
 | 
			
		||||
  LightState *state_parent_{nullptr};
 | 
			
		||||
  bool effect_active_{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AddressableLightTransformer : public LightTransitionTransformer {
 | 
			
		||||
@@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AddressableLight &light_;
 | 
			
		||||
  Color target_color_{};
 | 
			
		||||
  float last_transition_progress_{0.0f};
 | 
			
		||||
  float accumulated_alpha_{0.0f};
 | 
			
		||||
  Color target_color_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -69,8 +69,8 @@ class ESPColorCorrection {
 | 
			
		||||
 protected:
 | 
			
		||||
  uint8_t gamma_table_[256];
 | 
			
		||||
  uint8_t gamma_reverse_table_[256];
 | 
			
		||||
  uint8_t local_brightness_{255};
 | 
			
		||||
  Color max_brightness_;
 | 
			
		||||
  uint8_t local_brightness_{255};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,28 @@
 | 
			
		||||
#include "light_call.h"
 | 
			
		||||
#include "light_state.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/optional.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace light {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "light";
 | 
			
		||||
 | 
			
		||||
// Macro to reduce repetitive setter code
 | 
			
		||||
#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \
 | 
			
		||||
  LightCall &LightCall::set_##name(optional<type>(name)) { \
 | 
			
		||||
    if ((name).has_value()) { \
 | 
			
		||||
      this->name##_ = (name).value(); \
 | 
			
		||||
    } \
 | 
			
		||||
    this->set_flag_(flag, (name).has_value()); \
 | 
			
		||||
    return *this; \
 | 
			
		||||
  } \
 | 
			
		||||
  LightCall &LightCall::set_##name(type name) { \
 | 
			
		||||
    this->name##_ = name; \
 | 
			
		||||
    this->set_flag_(flag, true); \
 | 
			
		||||
    return *this; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
static const LogString *color_mode_to_human(ColorMode color_mode) {
 | 
			
		||||
  if (color_mode == ColorMode::UNKNOWN)
 | 
			
		||||
    return LOG_STR("Unknown");
 | 
			
		||||
@@ -32,41 +48,43 @@ void LightCall::perform() {
 | 
			
		||||
  const char *name = this->parent_->get_name().c_str();
 | 
			
		||||
  LightColorValues v = this->validate_();
 | 
			
		||||
 | 
			
		||||
  if (this->publish_) {
 | 
			
		||||
  if (this->get_publish_()) {
 | 
			
		||||
    ESP_LOGD(TAG, "'%s' Setting:", name);
 | 
			
		||||
 | 
			
		||||
    // Only print color mode when it's being changed
 | 
			
		||||
    ColorMode current_color_mode = this->parent_->remote_values.get_color_mode();
 | 
			
		||||
    if (this->color_mode_.value_or(current_color_mode) != current_color_mode) {
 | 
			
		||||
    ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode;
 | 
			
		||||
    if (target_color_mode != current_color_mode) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Only print state when it's being changed
 | 
			
		||||
    bool current_state = this->parent_->remote_values.is_on();
 | 
			
		||||
    if (this->state_.value_or(current_state) != current_state) {
 | 
			
		||||
    bool target_state = this->has_state() ? this->state_ : current_state;
 | 
			
		||||
    if (target_state != current_state) {
 | 
			
		||||
      ESP_LOGD(TAG, "  State: %s", ONOFF(v.is_on()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->brightness_.has_value()) {
 | 
			
		||||
    if (this->has_brightness()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Brightness: %.0f%%", v.get_brightness() * 100.0f);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->color_brightness_.has_value()) {
 | 
			
		||||
    if (this->has_color_brightness()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Color brightness: %.0f%%", v.get_color_brightness() * 100.0f);
 | 
			
		||||
    }
 | 
			
		||||
    if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) {
 | 
			
		||||
    if (this->has_red() || this->has_green() || this->has_blue()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
 | 
			
		||||
               v.get_blue() * 100.0f);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->white_.has_value()) {
 | 
			
		||||
    if (this->has_white()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  White: %.0f%%", v.get_white() * 100.0f);
 | 
			
		||||
    }
 | 
			
		||||
    if (this->color_temperature_.has_value()) {
 | 
			
		||||
    if (this->has_color_temperature()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Color temperature: %.1f mireds", v.get_color_temperature());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->cold_white_.has_value() || this->warm_white_.has_value()) {
 | 
			
		||||
    if (this->has_cold_white() || this->has_warm_white()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
 | 
			
		||||
               v.get_warm_white() * 100.0f);
 | 
			
		||||
    }
 | 
			
		||||
@@ -74,58 +92,57 @@ void LightCall::perform() {
 | 
			
		||||
 | 
			
		||||
  if (this->has_flash_()) {
 | 
			
		||||
    // FLASH
 | 
			
		||||
    if (this->publish_) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Flash length: %.1fs", *this->flash_length_ / 1e3f);
 | 
			
		||||
    if (this->get_publish_()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Flash length: %.1fs", this->flash_length_ / 1e3f);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->parent_->start_flash_(v, *this->flash_length_, this->publish_);
 | 
			
		||||
    this->parent_->start_flash_(v, this->flash_length_, this->get_publish_());
 | 
			
		||||
  } else if (this->has_transition_()) {
 | 
			
		||||
    // TRANSITION
 | 
			
		||||
    if (this->publish_) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Transition length: %.1fs", *this->transition_length_ / 1e3f);
 | 
			
		||||
    if (this->get_publish_()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Transition length: %.1fs", this->transition_length_ / 1e3f);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Special case: Transition and effect can be set when turning off
 | 
			
		||||
    if (this->has_effect_()) {
 | 
			
		||||
      if (this->publish_) {
 | 
			
		||||
      if (this->get_publish_()) {
 | 
			
		||||
        ESP_LOGD(TAG, "  Effect: 'None'");
 | 
			
		||||
      }
 | 
			
		||||
      this->parent_->stop_effect_();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->parent_->start_transition_(v, *this->transition_length_, this->publish_);
 | 
			
		||||
    this->parent_->start_transition_(v, this->transition_length_, this->get_publish_());
 | 
			
		||||
 | 
			
		||||
  } else if (this->has_effect_()) {
 | 
			
		||||
    // EFFECT
 | 
			
		||||
    auto effect = this->effect_;
 | 
			
		||||
    const char *effect_s;
 | 
			
		||||
    if (effect == 0u) {
 | 
			
		||||
    if (this->effect_ == 0u) {
 | 
			
		||||
      effect_s = "None";
 | 
			
		||||
    } else {
 | 
			
		||||
      effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str();
 | 
			
		||||
      effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->publish_) {
 | 
			
		||||
    if (this->get_publish_()) {
 | 
			
		||||
      ESP_LOGD(TAG, "  Effect: '%s'", effect_s);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->parent_->start_effect_(*this->effect_);
 | 
			
		||||
    this->parent_->start_effect_(this->effect_);
 | 
			
		||||
 | 
			
		||||
    // Also set light color values when starting an effect
 | 
			
		||||
    // For example to turn off the light
 | 
			
		||||
    this->parent_->set_immediately_(v, true);
 | 
			
		||||
  } else {
 | 
			
		||||
    // INSTANT CHANGE
 | 
			
		||||
    this->parent_->set_immediately_(v, this->publish_);
 | 
			
		||||
    this->parent_->set_immediately_(v, this->get_publish_());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!this->has_transition_()) {
 | 
			
		||||
    this->parent_->target_state_reached_callback_.call();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->publish_) {
 | 
			
		||||
  if (this->get_publish_()) {
 | 
			
		||||
    this->parent_->publish_state();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->save_) {
 | 
			
		||||
  if (this->get_save_()) {
 | 
			
		||||
    this->parent_->save_remote_values_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() {
 | 
			
		||||
  auto traits = this->parent_->get_traits();
 | 
			
		||||
 | 
			
		||||
  // Color mode check
 | 
			
		||||
  if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' does not support color mode %s", name,
 | 
			
		||||
             LOG_STR_ARG(color_mode_to_human(this->color_mode_.value())));
 | 
			
		||||
    this->color_mode_.reset();
 | 
			
		||||
  if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_MODE, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Ensure there is always a color mode set
 | 
			
		||||
  if (!this->color_mode_.has_value()) {
 | 
			
		||||
  if (!this->has_color_mode()) {
 | 
			
		||||
    this->color_mode_ = this->compute_color_mode_();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_MODE, true);
 | 
			
		||||
  }
 | 
			
		||||
  auto color_mode = *this->color_mode_;
 | 
			
		||||
  auto color_mode = this->color_mode_;
 | 
			
		||||
 | 
			
		||||
  // Transform calls that use non-native parameters for the current mode.
 | 
			
		||||
  this->transform_parameters_();
 | 
			
		||||
 | 
			
		||||
  // Brightness exists check
 | 
			
		||||
  if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
  if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': setting brightness not supported", name);
 | 
			
		||||
    this->brightness_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Transition length possible check
 | 
			
		||||
  if (this->transition_length_.has_value() && *this->transition_length_ != 0 &&
 | 
			
		||||
      !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
  if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': transitions not supported", name);
 | 
			
		||||
    this->transition_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Color brightness exists check
 | 
			
		||||
  if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
  if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name);
 | 
			
		||||
    this->color_brightness_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // RGB exists check
 | 
			
		||||
  if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) ||
 | 
			
		||||
      (this->blue_.has_value() && *this->blue_ > 0.0f)) {
 | 
			
		||||
  if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
 | 
			
		||||
      (this->has_blue() && this->blue_ > 0.0f)) {
 | 
			
		||||
    if (!(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name);
 | 
			
		||||
      this->red_.reset();
 | 
			
		||||
      this->green_.reset();
 | 
			
		||||
      this->blue_.reset();
 | 
			
		||||
      this->set_flag_(FLAG_HAS_RED, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_GREEN, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_BLUE, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // White value exists check
 | 
			
		||||
  if (this->white_.has_value() && *this->white_ > 0.0f &&
 | 
			
		||||
  if (this->has_white() && this->white_ > 0.0f &&
 | 
			
		||||
      !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name);
 | 
			
		||||
    this->white_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_WHITE, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Color temperature exists check
 | 
			
		||||
  if (this->color_temperature_.has_value() &&
 | 
			
		||||
  if (this->has_color_temperature() &&
 | 
			
		||||
      !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name);
 | 
			
		||||
    this->color_temperature_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Cold/warm white value exists check
 | 
			
		||||
  if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) ||
 | 
			
		||||
      (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) {
 | 
			
		||||
  if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
 | 
			
		||||
    if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name);
 | 
			
		||||
      this->cold_white_.reset();
 | 
			
		||||
      this->warm_white_.reset();
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLD_WHITE, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_WARM_WHITE, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
 | 
			
		||||
  if (name_##_.has_value()) { \
 | 
			
		||||
    auto val = *name_##_; \
 | 
			
		||||
  if (this->has_##name_()) { \
 | 
			
		||||
    auto val = this->name_##_; \
 | 
			
		||||
    if (val < (min) || val > (max)) { \
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \
 | 
			
		||||
               (min), (max)); \
 | 
			
		||||
      name_##_ = clamp(val, (min), (max)); \
 | 
			
		||||
      this->name_##_ = clamp(val, (min), (max)); \
 | 
			
		||||
    } \
 | 
			
		||||
  }
 | 
			
		||||
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
 | 
			
		||||
@@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() {
 | 
			
		||||
  VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
 | 
			
		||||
 | 
			
		||||
  // Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
 | 
			
		||||
  bool explicit_turn_off_request = this->state_.has_value() && !*this->state_;
 | 
			
		||||
  bool explicit_turn_off_request = this->has_state() && !this->state_;
 | 
			
		||||
 | 
			
		||||
  // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
 | 
			
		||||
  if (this->brightness_.has_value() && *this->brightness_ == 0.0f) {
 | 
			
		||||
    this->state_ = optional<float>(false);
 | 
			
		||||
    this->brightness_ = optional<float>(1.0f);
 | 
			
		||||
  if (this->has_brightness() && this->brightness_ == 0.0f) {
 | 
			
		||||
    this->state_ = false;
 | 
			
		||||
    this->set_flag_(FLAG_HAS_STATE, true);
 | 
			
		||||
    this->brightness_ = 1.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set color brightness to 100% if currently zero and a color is set.
 | 
			
		||||
  if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) {
 | 
			
		||||
    if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f)
 | 
			
		||||
      this->color_brightness_ = optional<float>(1.0f);
 | 
			
		||||
  if (this->has_red() || this->has_green() || this->has_blue()) {
 | 
			
		||||
    if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
 | 
			
		||||
      this->color_brightness_ = 1.0f;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Create color values for the light with this call applied.
 | 
			
		||||
  auto v = this->parent_->remote_values;
 | 
			
		||||
  if (this->color_mode_.has_value())
 | 
			
		||||
    v.set_color_mode(*this->color_mode_);
 | 
			
		||||
  if (this->state_.has_value())
 | 
			
		||||
    v.set_state(*this->state_);
 | 
			
		||||
  if (this->brightness_.has_value())
 | 
			
		||||
    v.set_brightness(*this->brightness_);
 | 
			
		||||
  if (this->color_brightness_.has_value())
 | 
			
		||||
    v.set_color_brightness(*this->color_brightness_);
 | 
			
		||||
  if (this->red_.has_value())
 | 
			
		||||
    v.set_red(*this->red_);
 | 
			
		||||
  if (this->green_.has_value())
 | 
			
		||||
    v.set_green(*this->green_);
 | 
			
		||||
  if (this->blue_.has_value())
 | 
			
		||||
    v.set_blue(*this->blue_);
 | 
			
		||||
  if (this->white_.has_value())
 | 
			
		||||
    v.set_white(*this->white_);
 | 
			
		||||
  if (this->color_temperature_.has_value())
 | 
			
		||||
    v.set_color_temperature(*this->color_temperature_);
 | 
			
		||||
  if (this->cold_white_.has_value())
 | 
			
		||||
    v.set_cold_white(*this->cold_white_);
 | 
			
		||||
  if (this->warm_white_.has_value())
 | 
			
		||||
    v.set_warm_white(*this->warm_white_);
 | 
			
		||||
  if (this->has_color_mode())
 | 
			
		||||
    v.set_color_mode(this->color_mode_);
 | 
			
		||||
  if (this->has_state())
 | 
			
		||||
    v.set_state(this->state_);
 | 
			
		||||
  if (this->has_brightness())
 | 
			
		||||
    v.set_brightness(this->brightness_);
 | 
			
		||||
  if (this->has_color_brightness())
 | 
			
		||||
    v.set_color_brightness(this->color_brightness_);
 | 
			
		||||
  if (this->has_red())
 | 
			
		||||
    v.set_red(this->red_);
 | 
			
		||||
  if (this->has_green())
 | 
			
		||||
    v.set_green(this->green_);
 | 
			
		||||
  if (this->has_blue())
 | 
			
		||||
    v.set_blue(this->blue_);
 | 
			
		||||
  if (this->has_white())
 | 
			
		||||
    v.set_white(this->white_);
 | 
			
		||||
  if (this->has_color_temperature())
 | 
			
		||||
    v.set_color_temperature(this->color_temperature_);
 | 
			
		||||
  if (this->has_cold_white())
 | 
			
		||||
    v.set_cold_white(this->cold_white_);
 | 
			
		||||
  if (this->has_warm_white())
 | 
			
		||||
    v.set_warm_white(this->warm_white_);
 | 
			
		||||
 | 
			
		||||
  v.normalize_color();
 | 
			
		||||
 | 
			
		||||
  // Flash length check
 | 
			
		||||
  if (this->has_flash_() && *this->flash_length_ == 0) {
 | 
			
		||||
  if (this->has_flash_() && this->flash_length_ == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name);
 | 
			
		||||
    this->flash_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_FLASH, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // validate transition length/flash length/effect not used at the same time
 | 
			
		||||
  bool supports_transition = color_mode & ColorCapability::BRIGHTNESS;
 | 
			
		||||
 | 
			
		||||
  // If effect is already active, remove effect start
 | 
			
		||||
  if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) {
 | 
			
		||||
    this->effect_.reset();
 | 
			
		||||
  if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
 | 
			
		||||
    this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // validate effect index
 | 
			
		||||
  if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_);
 | 
			
		||||
    this->effect_.reset();
 | 
			
		||||
  if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name);
 | 
			
		||||
    this->transition_length_.reset();
 | 
			
		||||
    this->flash_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_FLASH, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_flash_() && this->has_transition_()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name);
 | 
			
		||||
    this->transition_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) &&
 | 
			
		||||
  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
 | 
			
		||||
      supports_transition) {
 | 
			
		||||
    // nothing specified and light supports transitions, set default transition length
 | 
			
		||||
    this->transition_length_ = this->parent_->default_transition_length_;
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->transition_length_.value_or(0) == 0) {
 | 
			
		||||
  if (this->has_transition_() && this->transition_length_ == 0) {
 | 
			
		||||
    // 0 transition is interpreted as no transition (instant change)
 | 
			
		||||
    this->transition_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_transition_() && !supports_transition) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': transitions not supported", name);
 | 
			
		||||
    this->transition_length_.reset();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If not a flash and turning the light off, then disable the light
 | 
			
		||||
  // Do not use light color values directly, so that effects can set 0% brightness
 | 
			
		||||
  // Reason: When user turns off the light in frontend, the effect should also stop
 | 
			
		||||
  if (!this->has_flash_() && !this->state_.value_or(v.is_on())) {
 | 
			
		||||
  bool target_state = this->has_state() ? this->state_ : v.is_on();
 | 
			
		||||
  if (!this->has_flash_() && !target_state) {
 | 
			
		||||
    if (this->has_effect_()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name);
 | 
			
		||||
      this->effect_.reset();
 | 
			
		||||
      this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
    } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
 | 
			
		||||
      // Auto turn off effect
 | 
			
		||||
      this->effect_ = 0;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_EFFECT, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Disable saving for flashes
 | 
			
		||||
  if (this->has_flash_())
 | 
			
		||||
    this->save_ = false;
 | 
			
		||||
    this->set_flag_(FLAG_SAVE, false);
 | 
			
		||||
 | 
			
		||||
  return v;
 | 
			
		||||
}
 | 
			
		||||
@@ -343,24 +364,27 @@ void LightCall::transform_parameters_() {
 | 
			
		||||
  // - RGBWW lights with color_interlock=true, which also sets "brightness" and
 | 
			
		||||
  //   "color_temperature" (without color_interlock, CW/WW are set directly)
 | 
			
		||||
  // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature"
 | 
			
		||||
  if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) &&  //
 | 
			
		||||
      (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) &&                                       //
 | 
			
		||||
      !(*this->color_mode_ & ColorCapability::WHITE) &&                                                //
 | 
			
		||||
      !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) &&                                    //
 | 
			
		||||
  if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) &&  //
 | 
			
		||||
      (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) &&                         //
 | 
			
		||||
      !(this->color_mode_ & ColorCapability::WHITE) &&                                  //
 | 
			
		||||
      !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) &&                      //
 | 
			
		||||
      traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) {
 | 
			
		||||
    ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
 | 
			
		||||
             this->parent_->get_name().c_str());
 | 
			
		||||
    if (this->color_temperature_.has_value()) {
 | 
			
		||||
      const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
 | 
			
		||||
    if (this->has_color_temperature()) {
 | 
			
		||||
      const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
 | 
			
		||||
      const float ww_fraction =
 | 
			
		||||
          (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds());
 | 
			
		||||
      const float cw_fraction = 1.0f - ww_fraction;
 | 
			
		||||
      const float max_cw_ww = std::max(ww_fraction, cw_fraction);
 | 
			
		||||
      this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct());
 | 
			
		||||
      this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct());
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLD_WHITE, true);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_WARM_WHITE, true);
 | 
			
		||||
    }
 | 
			
		||||
    if (this->white_.has_value()) {
 | 
			
		||||
      this->brightness_ = *this->white_;
 | 
			
		||||
    if (this->has_white()) {
 | 
			
		||||
      this->brightness_ = this->white_;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -378,7 +402,7 @@ ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
 | 
			
		||||
  // Don't change if the light is being turned off.
 | 
			
		||||
  ColorMode current_mode = this->parent_->remote_values.get_color_mode();
 | 
			
		||||
  if (this->state_.has_value() && !*this->state_)
 | 
			
		||||
  if (this->has_state() && !this->state_)
 | 
			
		||||
    return current_mode;
 | 
			
		||||
 | 
			
		||||
  // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
 | 
			
		||||
@@ -411,12 +435,12 @@ ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
  return color_mode;
 | 
			
		||||
}
 | 
			
		||||
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
 | 
			
		||||
  bool has_white = this->white_.has_value() && *this->white_ > 0.0f;
 | 
			
		||||
  bool has_ct = this->color_temperature_.has_value();
 | 
			
		||||
  bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) ||
 | 
			
		||||
                  (this->warm_white_.has_value() && *this->warm_white_ > 0.0f);
 | 
			
		||||
  bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) ||
 | 
			
		||||
                 (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value());
 | 
			
		||||
  bool has_white = this->has_white() && this->white_ > 0.0f;
 | 
			
		||||
  bool has_ct = this->has_color_temperature();
 | 
			
		||||
  bool has_cwww =
 | 
			
		||||
      (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f);
 | 
			
		||||
  bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
 | 
			
		||||
                 (this->has_red() || this->has_green() || this->has_blue());
 | 
			
		||||
 | 
			
		||||
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
 | 
			
		||||
#define ENTRY(white, ct, cwww, rgb, ...) \
 | 
			
		||||
@@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) {
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
ColorMode LightCall::get_active_color_mode_() {
 | 
			
		||||
  return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode());
 | 
			
		||||
  return this->has_color_mode() ? this->color_mode_ : this->parent_->remote_values.get_color_mode();
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) {
 | 
			
		||||
  if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS)
 | 
			
		||||
@@ -505,7 +529,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) {
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) {
 | 
			
		||||
  if (this->parent_->get_traits().supports_color_mode(color_mode))
 | 
			
		||||
    this->color_mode_ = color_mode;
 | 
			
		||||
    this->set_color_mode(color_mode);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_brightness_if_supported(float brightness) {
 | 
			
		||||
@@ -549,110 +573,19 @@ LightCall &LightCall::set_warm_white_if_supported(float warm_white) {
 | 
			
		||||
    this->set_warm_white(warm_white);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_state(optional<bool> state) {
 | 
			
		||||
  this->state_ = state;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_state(bool state) {
 | 
			
		||||
  this->state_ = state;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_transition_length(optional<uint32_t> transition_length) {
 | 
			
		||||
  this->transition_length_ = transition_length;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_transition_length(uint32_t transition_length) {
 | 
			
		||||
  this->transition_length_ = transition_length;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_flash_length(optional<uint32_t> flash_length) {
 | 
			
		||||
  this->flash_length_ = flash_length;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_flash_length(uint32_t flash_length) {
 | 
			
		||||
  this->flash_length_ = flash_length;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_brightness(optional<float> brightness) {
 | 
			
		||||
  this->brightness_ = brightness;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_brightness(float brightness) {
 | 
			
		||||
  this->brightness_ = brightness;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_mode(optional<ColorMode> color_mode) {
 | 
			
		||||
  this->color_mode_ = color_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_mode(ColorMode color_mode) {
 | 
			
		||||
  this->color_mode_ = color_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_brightness(optional<float> brightness) {
 | 
			
		||||
  this->color_brightness_ = brightness;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_brightness(float brightness) {
 | 
			
		||||
  this->color_brightness_ = brightness;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_red(optional<float> red) {
 | 
			
		||||
  this->red_ = red;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_red(float red) {
 | 
			
		||||
  this->red_ = red;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_green(optional<float> green) {
 | 
			
		||||
  this->green_ = green;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_green(float green) {
 | 
			
		||||
  this->green_ = green;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_blue(optional<float> blue) {
 | 
			
		||||
  this->blue_ = blue;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_blue(float blue) {
 | 
			
		||||
  this->blue_ = blue;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_white(optional<float> white) {
 | 
			
		||||
  this->white_ = white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_white(float white) {
 | 
			
		||||
  this->white_ = white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_temperature(optional<float> color_temperature) {
 | 
			
		||||
  this->color_temperature_ = color_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_color_temperature(float color_temperature) {
 | 
			
		||||
  this->color_temperature_ = color_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_cold_white(optional<float> cold_white) {
 | 
			
		||||
  this->cold_white_ = cold_white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_cold_white(float cold_white) {
 | 
			
		||||
  this->cold_white_ = cold_white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_warm_white(optional<float> warm_white) {
 | 
			
		||||
  this->warm_white_ = warm_white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_warm_white(float warm_white) {
 | 
			
		||||
  this->warm_white_ = warm_white;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE)
 | 
			
		||||
IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE)
 | 
			
		||||
LightCall &LightCall::set_effect(optional<std::string> effect) {
 | 
			
		||||
  if (effect.has_value())
 | 
			
		||||
    this->set_effect(*effect);
 | 
			
		||||
@@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_effect(uint32_t effect_number) {
 | 
			
		||||
  this->effect_ = effect_number;
 | 
			
		||||
  this->set_flag_(FLAG_HAS_EFFECT, true);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {
 | 
			
		||||
  this->effect_ = effect_number;
 | 
			
		||||
  if (effect_number.has_value()) {
 | 
			
		||||
    this->effect_ = effect_number.value();
 | 
			
		||||
  }
 | 
			
		||||
  this->set_flag_(FLAG_HAS_EFFECT, effect_number.has_value());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_publish(bool publish) {
 | 
			
		||||
  this->publish_ = publish;
 | 
			
		||||
  this->set_flag_(FLAG_PUBLISH, publish);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_save(bool save) {
 | 
			
		||||
  this->save_ = save;
 | 
			
		||||
  this->set_flag_(FLAG_SAVE, save);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_rgb(float red, float green, float blue) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/optional.h"
 | 
			
		||||
#include "light_color_values.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
@@ -10,6 +9,11 @@ namespace light {
 | 
			
		||||
class LightState;
 | 
			
		||||
 | 
			
		||||
/** This class represents a requested change in a light state.
 | 
			
		||||
 *
 | 
			
		||||
 * Light state changes are tracked using a bitfield flags_ to minimize memory usage.
 | 
			
		||||
 * Each possible light property has a flag indicating whether it has been set.
 | 
			
		||||
 * This design keeps LightCall at ~56 bytes to minimize heap fragmentation on
 | 
			
		||||
 * ESP8266 and other memory-constrained devices.
 | 
			
		||||
 */
 | 
			
		||||
class LightCall {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -131,6 +135,19 @@ class LightCall {
 | 
			
		||||
  /// Set whether this light call should trigger a save state to recover them at startup..
 | 
			
		||||
  LightCall &set_save(bool save);
 | 
			
		||||
 | 
			
		||||
  // Getter methods to check if values are set
 | 
			
		||||
  bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; }
 | 
			
		||||
  bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; }
 | 
			
		||||
  bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; }
 | 
			
		||||
  bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; }
 | 
			
		||||
  bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; }
 | 
			
		||||
  bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; }
 | 
			
		||||
  bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; }
 | 
			
		||||
  bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; }
 | 
			
		||||
  bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; }
 | 
			
		||||
  bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; }
 | 
			
		||||
  bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; }
 | 
			
		||||
 | 
			
		||||
  /** Set the RGB color of the light by RGB values.
 | 
			
		||||
   *
 | 
			
		||||
   * Please note that this only changes the color of the light, not the brightness.
 | 
			
		||||
@@ -170,27 +187,62 @@ class LightCall {
 | 
			
		||||
  /// Some color modes also can be set using non-native parameters, transform those calls.
 | 
			
		||||
  void transform_parameters_();
 | 
			
		||||
 | 
			
		||||
  bool has_transition_() { return this->transition_length_.has_value(); }
 | 
			
		||||
  bool has_flash_() { return this->flash_length_.has_value(); }
 | 
			
		||||
  bool has_effect_() { return this->effect_.has_value(); }
 | 
			
		||||
  // Bitfield flags - each flag indicates whether a corresponding value has been set.
 | 
			
		||||
  enum FieldFlags : uint16_t {
 | 
			
		||||
    FLAG_HAS_STATE = 1 << 0,
 | 
			
		||||
    FLAG_HAS_TRANSITION = 1 << 1,
 | 
			
		||||
    FLAG_HAS_FLASH = 1 << 2,
 | 
			
		||||
    FLAG_HAS_EFFECT = 1 << 3,
 | 
			
		||||
    FLAG_HAS_BRIGHTNESS = 1 << 4,
 | 
			
		||||
    FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
 | 
			
		||||
    FLAG_HAS_RED = 1 << 6,
 | 
			
		||||
    FLAG_HAS_GREEN = 1 << 7,
 | 
			
		||||
    FLAG_HAS_BLUE = 1 << 8,
 | 
			
		||||
    FLAG_HAS_WHITE = 1 << 9,
 | 
			
		||||
    FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
 | 
			
		||||
    FLAG_HAS_COLD_WHITE = 1 << 11,
 | 
			
		||||
    FLAG_HAS_WARM_WHITE = 1 << 12,
 | 
			
		||||
    FLAG_HAS_COLOR_MODE = 1 << 13,
 | 
			
		||||
    FLAG_PUBLISH = 1 << 14,
 | 
			
		||||
    FLAG_SAVE = 1 << 15,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
 | 
			
		||||
  bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
 | 
			
		||||
  bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
 | 
			
		||||
  bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
 | 
			
		||||
  bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
 | 
			
		||||
 | 
			
		||||
  // Helper to set flag
 | 
			
		||||
  void set_flag_(FieldFlags flag, bool value) {
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this->flags_ |= flag;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->flags_ &= ~flag;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LightState *parent_;
 | 
			
		||||
  optional<bool> state_;
 | 
			
		||||
  optional<uint32_t> transition_length_;
 | 
			
		||||
  optional<uint32_t> flash_length_;
 | 
			
		||||
  optional<ColorMode> color_mode_;
 | 
			
		||||
  optional<float> brightness_;
 | 
			
		||||
  optional<float> color_brightness_;
 | 
			
		||||
  optional<float> red_;
 | 
			
		||||
  optional<float> green_;
 | 
			
		||||
  optional<float> blue_;
 | 
			
		||||
  optional<float> white_;
 | 
			
		||||
  optional<float> color_temperature_;
 | 
			
		||||
  optional<float> cold_white_;
 | 
			
		||||
  optional<float> warm_white_;
 | 
			
		||||
  optional<uint32_t> effect_;
 | 
			
		||||
  bool publish_{true};
 | 
			
		||||
  bool save_{true};
 | 
			
		||||
 | 
			
		||||
  // Light state values - use flags_ to check if a value has been set.
 | 
			
		||||
  // Group 4-byte aligned members first
 | 
			
		||||
  uint32_t transition_length_;
 | 
			
		||||
  uint32_t flash_length_;
 | 
			
		||||
  uint32_t effect_;
 | 
			
		||||
  float brightness_;
 | 
			
		||||
  float color_brightness_;
 | 
			
		||||
  float red_;
 | 
			
		||||
  float green_;
 | 
			
		||||
  float blue_;
 | 
			
		||||
  float white_;
 | 
			
		||||
  float color_temperature_;
 | 
			
		||||
  float cold_white_;
 | 
			
		||||
  float warm_white_;
 | 
			
		||||
 | 
			
		||||
  // Smaller members at the end for better packing
 | 
			
		||||
  uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE};  // Tracks which values are set
 | 
			
		||||
  ColorMode color_mode_;
 | 
			
		||||
  bool state_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -46,8 +46,7 @@ class LightColorValues {
 | 
			
		||||
 public:
 | 
			
		||||
  /// Construct the LightColorValues with all attributes enabled, but state set to off.
 | 
			
		||||
  LightColorValues()
 | 
			
		||||
      : color_mode_(ColorMode::UNKNOWN),
 | 
			
		||||
        state_(0.0f),
 | 
			
		||||
      : state_(0.0f),
 | 
			
		||||
        brightness_(1.0f),
 | 
			
		||||
        color_brightness_(1.0f),
 | 
			
		||||
        red_(1.0f),
 | 
			
		||||
@@ -56,7 +55,8 @@ class LightColorValues {
 | 
			
		||||
        white_(1.0f),
 | 
			
		||||
        color_temperature_{0.0f},
 | 
			
		||||
        cold_white_{1.0f},
 | 
			
		||||
        warm_white_{1.0f} {}
 | 
			
		||||
        warm_white_{1.0f},
 | 
			
		||||
        color_mode_(ColorMode::UNKNOWN) {}
 | 
			
		||||
 | 
			
		||||
  LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
 | 
			
		||||
                   float blue, float white, float color_temperature, float cold_white, float warm_white) {
 | 
			
		||||
@@ -292,7 +292,6 @@ class LightColorValues {
 | 
			
		||||
  void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  ColorMode color_mode_;
 | 
			
		||||
  float state_;  ///< ON / OFF, float for transition
 | 
			
		||||
  float brightness_;
 | 
			
		||||
  float color_brightness_;
 | 
			
		||||
@@ -303,6 +302,7 @@ class LightColorValues {
 | 
			
		||||
  float color_temperature_;  ///< Color Temperature in Mired
 | 
			
		||||
  float cold_white_;
 | 
			
		||||
  float warm_white_;
 | 
			
		||||
  ColorMode color_mode_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t {
 | 
			
		||||
struct LightStateRTCState {
 | 
			
		||||
  LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green,
 | 
			
		||||
                     float blue, float white, float color_temp, float cold_white, float warm_white)
 | 
			
		||||
      : color_mode(color_mode),
 | 
			
		||||
        state(state),
 | 
			
		||||
        brightness(brightness),
 | 
			
		||||
      : brightness(brightness),
 | 
			
		||||
        color_brightness(color_brightness),
 | 
			
		||||
        red(red),
 | 
			
		||||
        green(green),
 | 
			
		||||
@@ -41,10 +39,12 @@ struct LightStateRTCState {
 | 
			
		||||
        white(white),
 | 
			
		||||
        color_temp(color_temp),
 | 
			
		||||
        cold_white(cold_white),
 | 
			
		||||
        warm_white(warm_white) {}
 | 
			
		||||
        warm_white(warm_white),
 | 
			
		||||
        effect(0),
 | 
			
		||||
        color_mode(color_mode),
 | 
			
		||||
        state(state) {}
 | 
			
		||||
  LightStateRTCState() = default;
 | 
			
		||||
  ColorMode color_mode{ColorMode::UNKNOWN};
 | 
			
		||||
  bool state{false};
 | 
			
		||||
  // Group 4-byte aligned members first
 | 
			
		||||
  float brightness{1.0f};
 | 
			
		||||
  float color_brightness{1.0f};
 | 
			
		||||
  float red{1.0f};
 | 
			
		||||
@@ -55,6 +55,9 @@ struct LightStateRTCState {
 | 
			
		||||
  float cold_white{1.0f};
 | 
			
		||||
  float warm_white{1.0f};
 | 
			
		||||
  uint32_t effect{0};
 | 
			
		||||
  // Group smaller members at the end
 | 
			
		||||
  ColorMode color_mode{ColorMode::UNKNOWN};
 | 
			
		||||
  bool state{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** This class represents the communication layer between the front-end MQTT layer and the
 | 
			
		||||
@@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
  std::unique_ptr<LightTransformer> transformer_{nullptr};
 | 
			
		||||
  /// List of effects for this light.
 | 
			
		||||
  std::vector<LightEffect *> effects_;
 | 
			
		||||
  /// Object used to store the persisted values of the light.
 | 
			
		||||
  ESPPreferenceObject rtc_;
 | 
			
		||||
  /// Value for storing the index of the currently active effect. 0 if no effect is active
 | 
			
		||||
  uint32_t active_effect_index_{};
 | 
			
		||||
  /// Default transition length for all transitions in ms.
 | 
			
		||||
@@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
  uint32_t flash_transition_length_{};
 | 
			
		||||
  /// Gamma correction factor for the light.
 | 
			
		||||
  float gamma_correct_{};
 | 
			
		||||
 | 
			
		||||
  /// Whether the light value should be written in the next cycle.
 | 
			
		||||
  bool next_write_{true};
 | 
			
		||||
  // for effects, true if a transformer (transition) is active.
 | 
			
		||||
  bool is_transformer_active_ = false;
 | 
			
		||||
 | 
			
		||||
  /// Object used to store the persisted values of the light.
 | 
			
		||||
  ESPPreferenceObject rtc_;
 | 
			
		||||
 | 
			
		||||
  /** Callback to call when new values for the frontend are available.
 | 
			
		||||
   *
 | 
			
		||||
   * "Remote values" are light color values that are reported to the frontend and have a lower
 | 
			
		||||
 
 | 
			
		||||
@@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer {
 | 
			
		||||
  // transition from 0 to 1 on x = [0, 1]
 | 
			
		||||
  static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
 | 
			
		||||
 | 
			
		||||
  bool changing_color_mode_{false};
 | 
			
		||||
  LightColorValues end_values_{};
 | 
			
		||||
  LightColorValues intermediate_values_{};
 | 
			
		||||
  bool changing_color_mode_{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class LightFlashTransformer : public LightTransformer {
 | 
			
		||||
@@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  LightState &state_;
 | 
			
		||||
  uint32_t transition_length_;
 | 
			
		||||
  std::unique_ptr<LightTransformer> transformer_{nullptr};
 | 
			
		||||
  uint32_t transition_length_;
 | 
			
		||||
  bool begun_lightstate_restore_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ from esphome.components.libretiny.const import (
 | 
			
		||||
    COMPONENT_LN882X,
 | 
			
		||||
    COMPONENT_RTL87XX,
 | 
			
		||||
)
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ARGS,
 | 
			
		||||
@@ -42,6 +43,7 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_LN882X,
 | 
			
		||||
    PLATFORM_RP2040,
 | 
			
		||||
    PLATFORM_RTL87XX,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, Lambda, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -444,3 +446,25 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
 | 
			
		||||
 | 
			
		||||
    lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "logger_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
 | 
			
		||||
        "logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "logger_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "task_log_buffer.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,8 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
 | 
			
		||||
  if (this->baud_rate_ > 0) {
 | 
			
		||||
    this->write_msg_(this->tx_buffer_ + msg_start);
 | 
			
		||||
  }
 | 
			
		||||
  this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start);
 | 
			
		||||
  size_t msg_length = this->tx_buffer_at_ - msg_start - 1;  // -1 to exclude null terminator
 | 
			
		||||
  this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
 | 
			
		||||
 | 
			
		||||
  global_recursion_guard_ = false;
 | 
			
		||||
}
 | 
			
		||||
@@ -185,7 +186,8 @@ void Logger::loop() {
 | 
			
		||||
                                  this->tx_buffer_size_);
 | 
			
		||||
      this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
 | 
			
		||||
      this->tx_buffer_[this->tx_buffer_at_] = '\0';
 | 
			
		||||
      this->log_callback_.call(message->level, message->tag, this->tx_buffer_);
 | 
			
		||||
      size_t msg_len = this->tx_buffer_at_;  // We already know the length from tx_buffer_at_
 | 
			
		||||
      this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len);
 | 
			
		||||
      // At this point all the data we need from message has been transferred to the tx_buffer
 | 
			
		||||
      // so we can release the message to allow other tasks to use it as soon as possible.
 | 
			
		||||
      this->log_buffer_->release_message_main_loop(received_token);
 | 
			
		||||
@@ -214,7 +216,7 @@ void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->lo
 | 
			
		||||
UARTSelection Logger::get_uart() const { return this->uart_; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback) {
 | 
			
		||||
void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const char *, size_t)> &&callback) {
 | 
			
		||||
  this->log_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
 | 
			
		||||
 
 | 
			
		||||
@@ -143,7 +143,7 @@ class Logger : public Component {
 | 
			
		||||
  inline uint8_t level_for(const char *tag);
 | 
			
		||||
 | 
			
		||||
  /// Register a callback that will be called for every log message sent
 | 
			
		||||
  void add_on_log_callback(std::function<void(uint8_t, const char *, const char *)> &&callback);
 | 
			
		||||
  void add_on_log_callback(std::function<void(uint8_t, const char *, const char *, size_t)> &&callback);
 | 
			
		||||
 | 
			
		||||
  // add a listener for log level changes
 | 
			
		||||
  void add_listener(std::function<void(uint8_t)> &&callback) { this->level_callback_.add(std::move(callback)); }
 | 
			
		||||
@@ -192,7 +192,7 @@ class Logger : public Component {
 | 
			
		||||
    if (this->baud_rate_ > 0) {
 | 
			
		||||
      this->write_msg_(this->tx_buffer_);  // If logging is enabled, write to console
 | 
			
		||||
    }
 | 
			
		||||
    this->log_callback_.call(level, tag, this->tx_buffer_);
 | 
			
		||||
    this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Write the body of the log message to the buffer
 | 
			
		||||
@@ -246,7 +246,7 @@ class Logger : public Component {
 | 
			
		||||
 | 
			
		||||
  // Large objects (internally aligned)
 | 
			
		||||
  std::map<std::string, uint8_t> log_levels_{};
 | 
			
		||||
  CallbackManager<void(uint8_t, const char *, const char *)> log_callback_{};
 | 
			
		||||
  CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
 | 
			
		||||
  CallbackManager<void(uint8_t)> level_callback_{};
 | 
			
		||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
 | 
			
		||||
  std::unique_ptr<logger::TaskLogBuffer> log_buffer_;  // Will be initialized with init_log_buffer
 | 
			
		||||
@@ -355,7 +355,7 @@ class Logger : public Component {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
 | 
			
		||||
    static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR);
 | 
			
		||||
    static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
 | 
			
		||||
    this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -385,7 +385,7 @@ class LoggerMessageTrigger : public Trigger<uint8_t, const char *, const char *>
 | 
			
		||||
 public:
 | 
			
		||||
  explicit LoggerMessageTrigger(Logger *parent, uint8_t level) {
 | 
			
		||||
    this->level_ = level;
 | 
			
		||||
    parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) {
 | 
			
		||||
    parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
      if (level <= this->level_) {
 | 
			
		||||
        this->trigger(level, tag, message);
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -184,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) {
 | 
			
		||||
  ) {
 | 
			
		||||
    puts(msg);
 | 
			
		||||
  } else {
 | 
			
		||||
    uart_write_bytes(this->uart_num_, msg, strlen(msg));
 | 
			
		||||
    // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
 | 
			
		||||
    size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
 | 
			
		||||
    uart_write_bytes(this->uart_num_, msg, len);
 | 
			
		||||
    uart_write_bytes(this->uart_num_, "\n", 1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.esp32 import add_idf_component
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DISABLED,
 | 
			
		||||
@@ -8,6 +9,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_PROTOCOL,
 | 
			
		||||
    CONF_SERVICE,
 | 
			
		||||
    CONF_SERVICES,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -108,3 +110,21 @@ async def to_code(config):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        cg.add(var.add_extra_service(exp))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "mdns_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "mdns_host.cpp": {PlatformFramework.HOST_NATIVE},
 | 
			
		||||
        "mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "mdns_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ from esphome.automation import Condition
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import logger
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AVAILABILITY,
 | 
			
		||||
@@ -54,6 +55,7 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_BK72XX,
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -596,3 +598,13 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def mqtt_disable_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "mqtt_backend_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -57,14 +57,15 @@ void MQTTClientComponent::setup() {
 | 
			
		||||
  });
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
 | 
			
		||||
      if (level <= this->log_level_ && this->is_connected()) {
 | 
			
		||||
        this->publish({.topic = this->log_message_.topic,
 | 
			
		||||
                       .payload = message,
 | 
			
		||||
                       .qos = this->log_message_.qos,
 | 
			
		||||
                       .retain = this->log_message_.retain});
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    logger::global_logger->add_on_log_callback(
 | 
			
		||||
        [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
          if (level <= this->log_level_ && this->is_connected()) {
 | 
			
		||||
            this->publish({.topic = this->log_message_.topic,
 | 
			
		||||
                           .payload = std::string(message, message_len),
 | 
			
		||||
                           .qos = this->log_message_.qos,
 | 
			
		||||
                           .retain = this->log_message_.retain});
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
from esphome.const import PlatformFramework
 | 
			
		||||
 | 
			
		||||
nextion_ns = cg.esphome_ns.namespace("nextion")
 | 
			
		||||
Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice)
 | 
			
		||||
@@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref")
 | 
			
		||||
CONF_NEXTION_ID = "nextion_id"
 | 
			
		||||
CONF_PUBLISH_STATE = "publish_state"
 | 
			
		||||
CONF_SEND_TO_NEXTION = "send_to_nextion"
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "nextion_upload_arduino.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.RP2040_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
@@ -7,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_OTA,
 | 
			
		||||
    CONF_PLATFORM,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -120,3 +122,18 @@ async def ota_to_code(var, config):
 | 
			
		||||
        use_state_callback = True
 | 
			
		||||
    if use_state_callback:
 | 
			
		||||
        cg.add_define("USE_OTA_STATE_CALLBACK")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO},
 | 
			
		||||
        "ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
        "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "ota_backend_arduino_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32, esp32_rmt, remote_base
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BUFFER_SIZE,
 | 
			
		||||
@@ -15,6 +16,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    CONF_USE_DMA,
 | 
			
		||||
    CONF_VALUE,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, TimePeriod
 | 
			
		||||
 | 
			
		||||
@@ -170,3 +172,19 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
 | 
			
		||||
    cg.add(var.set_filter_us(config[CONF_FILTER]))
 | 
			
		||||
    cg.add(var.set_idle_us(config[CONF_IDLE]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "remote_receiver_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "remote_receiver_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from esphome import automation, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32, esp32_rmt, remote_base
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CARRIER_DUTY_PERCENT,
 | 
			
		||||
@@ -12,6 +13,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_RMT_SYMBOLS,
 | 
			
		||||
    CONF_USE_DMA,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
@@ -95,3 +97,19 @@ async def to_code(config):
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            var.get_complete_trigger(), [], on_complete_config
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "remote_transmitter_esp32.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "remote_transmitter_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								esphome/components/rp2040/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/rp2040/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_RP2040
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
#if defined(USE_WIFI)
 | 
			
		||||
#include <WiFi.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <hardware/structs/rosc.h>
 | 
			
		||||
#include <hardware/sync.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
uint32_t random_uint32() {
 | 
			
		||||
  uint32_t result = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < 32; i++) {
 | 
			
		||||
    result <<= 1;
 | 
			
		||||
    result |= rosc_hw->randombit;
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  while (len-- != 0) {
 | 
			
		||||
    uint8_t result = 0;
 | 
			
		||||
    for (uint8_t i = 0; i < 8; i++) {
 | 
			
		||||
      result <<= 1;
 | 
			
		||||
      result |= rosc_hw->randombit;
 | 
			
		||||
    }
 | 
			
		||||
    *data++ = result;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
 | 
			
		||||
Mutex::Mutex() {}
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() {}
 | 
			
		||||
bool Mutex::try_lock() { return true; }
 | 
			
		||||
void Mutex::unlock() {}
 | 
			
		||||
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
 | 
			
		||||
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
  WiFi.macAddress(mac);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_RP2040
 | 
			
		||||
							
								
								
									
										26
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
"""
 | 
			
		||||
Runtime statistics component for ESPHome.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = []
 | 
			
		||||
 | 
			
		||||
CONF_ENABLED = "enabled"
 | 
			
		||||
CONF_LOG_INTERVAL = "log_interval"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_ENABLED, default=True): cv.boolean,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_LOG_INTERVAL, default=60000
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    """Generate code for the runtime statistics component."""
 | 
			
		||||
    cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
 | 
			
		||||
    cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))
 | 
			
		||||
@@ -118,7 +118,7 @@ optional<float> QuantileFilter::new_value(float value) {
 | 
			
		||||
      size_t queue_size = quantile_queue.size();
 | 
			
		||||
      if (queue_size) {
 | 
			
		||||
        size_t position = ceilf(queue_size * this->quantile_) - 1;
 | 
			
		||||
        ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size);
 | 
			
		||||
        ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
 | 
			
		||||
        result = quantile_queue[position];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
@@ -40,3 +41,18 @@ async def to_code(config):
 | 
			
		||||
    elif impl == IMPLEMENTATION_BSD_SOCKETS:
 | 
			
		||||
        cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
 | 
			
		||||
        cg.add_define("USE_SOCKET_SELECT_SUPPORT")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def FILTER_SOURCE_FILES() -> list[str]:
 | 
			
		||||
    """Return list of socket implementation files that aren't selected by the user."""
 | 
			
		||||
    impl = CORE.config["socket"][CONF_IMPLEMENTATION]
 | 
			
		||||
 | 
			
		||||
    # Build list of files to exclude based on selected implementation
 | 
			
		||||
    excluded = []
 | 
			
		||||
    if impl != IMPLEMENTATION_LWIP_TCP:
 | 
			
		||||
        excluded.append("lwip_raw_tcp_impl.cpp")
 | 
			
		||||
    if impl != IMPLEMENTATION_BSD_SOCKETS:
 | 
			
		||||
        excluded.append("bsd_sockets_impl.cpp")
 | 
			
		||||
    if impl != IMPLEMENTATION_LWIP_SOCKETS:
 | 
			
		||||
        excluded.append("lwip_sockets_impl.cpp")
 | 
			
		||||
    return excluded
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CLK_PIN,
 | 
			
		||||
@@ -31,6 +32,7 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PLATFORM_RP2040,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
@@ -423,3 +425,18 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso:
 | 
			
		||||
        {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)},
 | 
			
		||||
        extra=cv.ALLOW_EXTRA,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "spi_arduino.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.RP2040_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
 | 
			
		||||
 | 
			
		||||
void Syslog::setup() {
 | 
			
		||||
  logger::global_logger->add_on_log_callback(
 | 
			
		||||
      [this](int level, const char *tag, const char *message) { this->log_(level, tag, message); });
 | 
			
		||||
      [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
        this->log_(level, tag, message, message_len);
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Syslog::log_(const int level, const char *tag, const char *message) const {
 | 
			
		||||
void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const {
 | 
			
		||||
  if (level > this->log_level_)
 | 
			
		||||
    return;
 | 
			
		||||
  // Syslog PRI calculation: facility * 8 + severity
 | 
			
		||||
@@ -34,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message) const {
 | 
			
		||||
  }
 | 
			
		||||
  int pri = this->facility_ * 8 + severity;
 | 
			
		||||
  auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S");
 | 
			
		||||
  unsigned len = strlen(message);
 | 
			
		||||
  size_t len = message_len;
 | 
			
		||||
  // remove color formatting
 | 
			
		||||
  if (this->strip_ && message[0] == 0x1B && len > 11) {
 | 
			
		||||
    message += 7;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ class Syslog : public Component, public Parented<udp::UDPComponent> {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int log_level_;
 | 
			
		||||
  void log_(int level, const char *tag, const char *message) const;
 | 
			
		||||
  void log_(int level, const char *tag, const char *message, size_t message_len) const;
 | 
			
		||||
  time::RealTimeClock *time_;
 | 
			
		||||
  bool strip_{true};
 | 
			
		||||
  int facility_{16};
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import re
 | 
			
		||||
 | 
			
		||||
from esphome import automation, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AFTER,
 | 
			
		||||
@@ -27,6 +28,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TX_PIN,
 | 
			
		||||
    CONF_UART_ID,
 | 
			
		||||
    PLATFORM_HOST,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
@@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_data_static(data))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
 | 
			
		||||
        "uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
        "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE},
 | 
			
		||||
        "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
        "uart_component_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -287,7 +287,8 @@ void WebServer::setup() {
 | 
			
		||||
  if (logger::global_logger != nullptr && this->expose_log_) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback(
 | 
			
		||||
        // logs are not deferred, the memory overhead would be too large
 | 
			
		||||
        [this](int level, const char *tag, const char *message) {
 | 
			
		||||
        [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
          (void) message_len;
 | 
			
		||||
          this->events_.try_send_nodefer(message, "log", millis());
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -40,4 +40,7 @@ async def to_code(config):
 | 
			
		||||
        if CORE.is_esp8266:
 | 
			
		||||
            cg.add_library("ESP8266WiFi", None)
 | 
			
		||||
        # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
 | 
			
		||||
        cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
 | 
			
		||||
        # Use fork with libretiny compatibility fix
 | 
			
		||||
        cg.add_library(
 | 
			
		||||
            "https://github.com/bdraco/ESPAsyncWebServer.git#libretiny_Fix", None
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ from esphome.automation import Condition
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
 | 
			
		||||
from esphome.components.network import IPAddress
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AP,
 | 
			
		||||
@@ -39,6 +40,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TTLS_PHASE_2,
 | 
			
		||||
    CONF_USE_ADDRESS,
 | 
			
		||||
    CONF_USERNAME,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, HexInt, coroutine_with_priority
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
@@ -526,3 +528,18 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        await automation.build_automation(var.get_error_trigger(), [], on_error_config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
 | 
			
		||||
        "wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
        "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "wifi_component_libretiny.cpp": {
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,18 @@
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    KEY_CORE,
 | 
			
		||||
    KEY_TARGET_FRAMEWORK,
 | 
			
		||||
    KEY_TARGET_PLATFORM,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
 | 
			
		||||
_PLATFORM_FRAMEWORK_LOOKUP = {
 | 
			
		||||
    (pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Extend:
 | 
			
		||||
@@ -103,3 +117,43 @@ def merge_config(full_old, full_new):
 | 
			
		||||
        return new
 | 
			
		||||
 | 
			
		||||
    return merge(full_old, full_new)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_source_files_from_platform(
 | 
			
		||||
    files_map: dict[str, set[PlatformFramework]],
 | 
			
		||||
) -> Callable[[], list[str]]:
 | 
			
		||||
    """Helper to build a FILTER_SOURCE_FILES function from platform mapping.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        files_map: Dict mapping filename to set of PlatformFramework enums
 | 
			
		||||
                  that should compile this file
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Function that returns list of files to exclude for current platform
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def filter_source_files() -> list[str]:
 | 
			
		||||
        # Get current platform/framework
 | 
			
		||||
        core_data = CORE.data.get(KEY_CORE, {})
 | 
			
		||||
        target_platform = core_data.get(KEY_TARGET_PLATFORM)
 | 
			
		||||
        target_framework = core_data.get(KEY_TARGET_FRAMEWORK)
 | 
			
		||||
 | 
			
		||||
        if not target_platform or not target_framework:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # Direct lookup of current PlatformFramework
 | 
			
		||||
        current_platform_framework = _PLATFORM_FRAMEWORK_LOOKUP.get(
 | 
			
		||||
            (target_platform, target_framework)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if not current_platform_framework:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # Return files that should be excluded for current platform
 | 
			
		||||
        return [
 | 
			
		||||
            filename
 | 
			
		||||
            for filename, platforms in files_map.items()
 | 
			
		||||
            if current_platform_framework not in platforms
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    return filter_source_files
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
"""Constants used by esphome."""
 | 
			
		||||
 | 
			
		||||
from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.7.0-dev"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
@@ -7,14 +11,55 @@ VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
PLATFORM_BK72XX = "bk72xx"
 | 
			
		||||
PLATFORM_ESP32 = "esp32"
 | 
			
		||||
PLATFORM_ESP8266 = "esp8266"
 | 
			
		||||
PLATFORM_HOST = "host"
 | 
			
		||||
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
 | 
			
		||||
PLATFORM_LN882X = "ln882x"
 | 
			
		||||
PLATFORM_RP2040 = "rp2040"
 | 
			
		||||
PLATFORM_RTL87XX = "rtl87xx"
 | 
			
		||||
 | 
			
		||||
class Platform(StrEnum):
 | 
			
		||||
    """Platform identifiers for ESPHome."""
 | 
			
		||||
 | 
			
		||||
    BK72XX = "bk72xx"
 | 
			
		||||
    ESP32 = "esp32"
 | 
			
		||||
    ESP8266 = "esp8266"
 | 
			
		||||
    HOST = "host"
 | 
			
		||||
    LIBRETINY_OLDSTYLE = "libretiny"
 | 
			
		||||
    LN882X = "ln882x"
 | 
			
		||||
    RP2040 = "rp2040"
 | 
			
		||||
    RTL87XX = "rtl87xx"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Framework(StrEnum):
 | 
			
		||||
    """Framework identifiers for ESPHome."""
 | 
			
		||||
 | 
			
		||||
    ARDUINO = "arduino"
 | 
			
		||||
    ESP_IDF = "esp-idf"
 | 
			
		||||
    NATIVE = "host"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlatformFramework(Enum):
 | 
			
		||||
    """Combined platform-framework identifiers with tuple values."""
 | 
			
		||||
 | 
			
		||||
    # ESP32 variants
 | 
			
		||||
    ESP32_ARDUINO = (Platform.ESP32, Framework.ARDUINO)
 | 
			
		||||
    ESP32_IDF = (Platform.ESP32, Framework.ESP_IDF)
 | 
			
		||||
 | 
			
		||||
    # Arduino framework platforms
 | 
			
		||||
    ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO)
 | 
			
		||||
    RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO)
 | 
			
		||||
    BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO)
 | 
			
		||||
    RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
 | 
			
		||||
    LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
 | 
			
		||||
 | 
			
		||||
    # Host platform (native)
 | 
			
		||||
    HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Maintain backward compatibility by reassigning after enum definition
 | 
			
		||||
PLATFORM_BK72XX = Platform.BK72XX
 | 
			
		||||
PLATFORM_ESP32 = Platform.ESP32
 | 
			
		||||
PLATFORM_ESP8266 = Platform.ESP8266
 | 
			
		||||
PLATFORM_HOST = Platform.HOST
 | 
			
		||||
PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
 | 
			
		||||
PLATFORM_LN882X = Platform.LN882X
 | 
			
		||||
PLATFORM_RP2040 = Platform.RP2040
 | 
			
		||||
PLATFORM_RTL87XX = Platform.RTL87XX
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
 | 
			
		||||
 
 | 
			
		||||
@@ -141,6 +141,10 @@ void Application::loop() {
 | 
			
		||||
  this->in_loop_ = false;
 | 
			
		||||
  this->app_state_ = new_app_state;
 | 
			
		||||
 | 
			
		||||
  // Process any pending runtime stats printing after all components have run
 | 
			
		||||
  // This ensures stats printing doesn't affect component timing measurements
 | 
			
		||||
  runtime_stats.process_pending_stats(last_op_end_time);
 | 
			
		||||
 | 
			
		||||
  // Use the last component's end time instead of calling millis() again
 | 
			
		||||
  auto elapsed = last_op_end_time - this->last_loop_;
 | 
			
		||||
  if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/core/runtime_stats.h"
 | 
			
		||||
#include "esphome/core/scheduler.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
@@ -348,6 +349,18 @@ class Application {
 | 
			
		||||
 | 
			
		||||
  uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
 | 
			
		||||
 | 
			
		||||
  /** Enable or disable runtime statistics collection.
 | 
			
		||||
   *
 | 
			
		||||
   * @param enable Whether to enable runtime statistics collection.
 | 
			
		||||
   */
 | 
			
		||||
  void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
 | 
			
		||||
 | 
			
		||||
  /** Set the interval at which runtime statistics are logged.
 | 
			
		||||
   *
 | 
			
		||||
   * @param interval The interval in milliseconds between logging of runtime statistics.
 | 
			
		||||
   */
 | 
			
		||||
  void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
 | 
			
		||||
 | 
			
		||||
  void schedule_dump_config() { this->dump_config_at_ = 0; }
 | 
			
		||||
 | 
			
		||||
  void feed_wdt(uint32_t time = 0);
 | 
			
		||||
@@ -368,6 +381,17 @@ class Application {
 | 
			
		||||
 | 
			
		||||
  uint8_t get_app_state() const { return this->app_state_; }
 | 
			
		||||
 | 
			
		||||
// Helper macro for entity getter method declarations - reduces code duplication
 | 
			
		||||
// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter
 | 
			
		||||
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
 | 
			
		||||
  entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
 | 
			
		||||
    for (auto *obj : this->entities_member##_) { \
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
 | 
			
		||||
        return obj; \
 | 
			
		||||
    } \
 | 
			
		||||
    return nullptr; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
  const std::vector<Device *> &get_devices() { return this->devices_; }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -376,218 +400,92 @@ class Application {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
 | 
			
		||||
  binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->binary_sensors_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  const std::vector<switch_::Switch *> &get_switches() { return this->switches_; }
 | 
			
		||||
  switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->switches_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(switch_::Switch, switch, switches)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  const std::vector<button::Button *> &get_buttons() { return this->buttons_; }
 | 
			
		||||
  button::Button *get_button_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->buttons_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(button::Button, button, buttons)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; }
 | 
			
		||||
  sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->sensors_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  const std::vector<text_sensor::TextSensor *> &get_text_sensors() { return this->text_sensors_; }
 | 
			
		||||
  text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->text_sensors_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  const std::vector<fan::Fan *> &get_fans() { return this->fans_; }
 | 
			
		||||
  fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->fans_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(fan::Fan, fan, fans)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  const std::vector<cover::Cover *> &get_covers() { return this->covers_; }
 | 
			
		||||
  cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->covers_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(cover::Cover, cover, covers)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  const std::vector<light::LightState *> &get_lights() { return this->lights_; }
 | 
			
		||||
  light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->lights_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(light::LightState, light, lights)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  const std::vector<climate::Climate *> &get_climates() { return this->climates_; }
 | 
			
		||||
  climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->climates_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(climate::Climate, climate, climates)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  const std::vector<number::Number *> &get_numbers() { return this->numbers_; }
 | 
			
		||||
  number::Number *get_number_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->numbers_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(number::Number, number, numbers)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
  const std::vector<datetime::DateEntity *> &get_dates() { return this->dates_; }
 | 
			
		||||
  datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->dates_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  const std::vector<datetime::TimeEntity *> &get_times() { return this->times_; }
 | 
			
		||||
  datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->times_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
  const std::vector<datetime::DateTimeEntity *> &get_datetimes() { return this->datetimes_; }
 | 
			
		||||
  datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->datetimes_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  const std::vector<text::Text *> &get_texts() { return this->texts_; }
 | 
			
		||||
  text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->texts_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(text::Text, text, texts)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  const std::vector<select::Select *> &get_selects() { return this->selects_; }
 | 
			
		||||
  select::Select *get_select_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->selects_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(select::Select, select, selects)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  const std::vector<lock::Lock *> &get_locks() { return this->locks_; }
 | 
			
		||||
  lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->locks_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(lock::Lock, lock, locks)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  const std::vector<valve::Valve *> &get_valves() { return this->valves_; }
 | 
			
		||||
  valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->valves_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(valve::Valve, valve, valves)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  const std::vector<media_player::MediaPlayer *> &get_media_players() { return this->media_players_; }
 | 
			
		||||
  media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->media_players_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
  const std::vector<alarm_control_panel::AlarmControlPanel *> &get_alarm_control_panels() {
 | 
			
		||||
    return this->alarm_control_panels_;
 | 
			
		||||
  }
 | 
			
		||||
  alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->alarm_control_panels_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
  const std::vector<event::Event *> &get_events() { return this->events_; }
 | 
			
		||||
  event::Event *get_event_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->events_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(event::Event, event, events)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  const std::vector<update::UpdateEntity *> &get_updates() { return this->updates_; }
 | 
			
		||||
  update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) {
 | 
			
		||||
    for (auto *obj : this->updates_) {
 | 
			
		||||
      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  Scheduler scheduler;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,17 +26,17 @@ static const char *const TAG = "component";
 | 
			
		||||
// 1. Components are never destroyed in ESPHome
 | 
			
		||||
// 2. Failed components remain failed (no recovery mechanism)
 | 
			
		||||
// 3. Memory usage is minimal (only failures with custom messages are stored)
 | 
			
		||||
static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> &get_component_error_messages() {
 | 
			
		||||
  static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> instance;
 | 
			
		||||
  return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
 | 
			
		||||
// This is safe because ESPHome is single-threaded during initialization
 | 
			
		||||
namespace {
 | 
			
		||||
// Error messages for failed components
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> component_error_messages;
 | 
			
		||||
// Setup priority overrides - freed after setup completes
 | 
			
		||||
// Typically < 5 entries, lazy allocated
 | 
			
		||||
static std::unique_ptr<std::vector<std::pair<const Component *, float>>> &get_setup_priority_overrides() {
 | 
			
		||||
  static std::unique_ptr<std::vector<std::pair<const Component *, float>>> instance;
 | 
			
		||||
  return instance;
 | 
			
		||||
}
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
std::unique_ptr<std::vector<std::pair<const Component *, float>>> setup_priority_overrides;
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
namespace setup_priority {
 | 
			
		||||
 | 
			
		||||
@@ -130,8 +130,8 @@ void Component::call_dump_config() {
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    // Look up error message from global vector
 | 
			
		||||
    const char *error_msg = "unspecified";
 | 
			
		||||
    if (get_component_error_messages()) {
 | 
			
		||||
      for (const auto &pair : *get_component_error_messages()) {
 | 
			
		||||
    if (component_error_messages) {
 | 
			
		||||
      for (const auto &pair : *component_error_messages) {
 | 
			
		||||
        if (pair.first == this) {
 | 
			
		||||
          error_msg = pair.second;
 | 
			
		||||
          break;
 | 
			
		||||
@@ -285,18 +285,18 @@ void Component::status_set_error(const char *message) {
 | 
			
		||||
  ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
 | 
			
		||||
  if (strcmp(message, "unspecified") != 0) {
 | 
			
		||||
    // Lazy allocate the error messages vector if needed
 | 
			
		||||
    if (!get_component_error_messages()) {
 | 
			
		||||
      get_component_error_messages() = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();
 | 
			
		||||
    if (!component_error_messages) {
 | 
			
		||||
      component_error_messages = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();
 | 
			
		||||
    }
 | 
			
		||||
    // Check if this component already has an error message
 | 
			
		||||
    for (auto &pair : *get_component_error_messages()) {
 | 
			
		||||
    for (auto &pair : *component_error_messages) {
 | 
			
		||||
      if (pair.first == this) {
 | 
			
		||||
        pair.second = message;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Add new error message
 | 
			
		||||
    get_component_error_messages()->emplace_back(this, message);
 | 
			
		||||
    component_error_messages->emplace_back(this, message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Component::status_clear_warning() {
 | 
			
		||||
@@ -322,9 +322,9 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
 | 
			
		||||
void Component::dump_config() {}
 | 
			
		||||
float Component::get_actual_setup_priority() const {
 | 
			
		||||
  // Check if there's an override in the global vector
 | 
			
		||||
  if (get_setup_priority_overrides()) {
 | 
			
		||||
  if (setup_priority_overrides) {
 | 
			
		||||
    // Linear search is fine for small n (typically < 5 overrides)
 | 
			
		||||
    for (const auto &pair : *get_setup_priority_overrides()) {
 | 
			
		||||
    for (const auto &pair : *setup_priority_overrides) {
 | 
			
		||||
      if (pair.first == this) {
 | 
			
		||||
        return pair.second;
 | 
			
		||||
      }
 | 
			
		||||
@@ -334,14 +334,14 @@ float Component::get_actual_setup_priority() const {
 | 
			
		||||
}
 | 
			
		||||
void Component::set_setup_priority(float priority) {
 | 
			
		||||
  // Lazy allocate the vector if needed
 | 
			
		||||
  if (!get_setup_priority_overrides()) {
 | 
			
		||||
    get_setup_priority_overrides() = std::make_unique<std::vector<std::pair<const Component *, float>>>();
 | 
			
		||||
  if (!setup_priority_overrides) {
 | 
			
		||||
    setup_priority_overrides = std::make_unique<std::vector<std::pair<const Component *, float>>>();
 | 
			
		||||
    // Reserve some space to avoid reallocations (most configs have < 10 overrides)
 | 
			
		||||
    get_setup_priority_overrides()->reserve(10);
 | 
			
		||||
    setup_priority_overrides->reserve(10);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check if this component already has an override
 | 
			
		||||
  for (auto &pair : *get_setup_priority_overrides()) {
 | 
			
		||||
  for (auto &pair : *setup_priority_overrides) {
 | 
			
		||||
    if (pair.first == this) {
 | 
			
		||||
      pair.second = priority;
 | 
			
		||||
      return;
 | 
			
		||||
@@ -349,7 +349,7 @@ void Component::set_setup_priority(float priority) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add new override
 | 
			
		||||
  get_setup_priority_overrides()->emplace_back(this, priority);
 | 
			
		||||
  setup_priority_overrides->emplace_back(this, priority);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Component::has_overridden_loop() const {
 | 
			
		||||
@@ -395,6 +395,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
 | 
			
		||||
  uint32_t curr_time = millis();
 | 
			
		||||
 | 
			
		||||
  uint32_t blocking_time = curr_time - this->started_;
 | 
			
		||||
 | 
			
		||||
  // Record component runtime stats
 | 
			
		||||
  runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
 | 
			
		||||
  bool should_warn;
 | 
			
		||||
  if (this->component_ != nullptr) {
 | 
			
		||||
    should_warn = this->component_->should_warn_of_blocking(blocking_time);
 | 
			
		||||
@@ -414,7 +417,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
 | 
			
		||||
 | 
			
		||||
void clear_setup_priority_overrides() {
 | 
			
		||||
  // Free the setup priority map completely
 | 
			
		||||
  get_setup_priority_overrides().reset();
 | 
			
		||||
  setup_priority_overrides.reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/optional.h"
 | 
			
		||||
#include "esphome/core/runtime_stats.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from esphome import automation, core
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AREA,
 | 
			
		||||
@@ -35,6 +36,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
    KEY_CORE,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
    __version__ as ESPHOME_VERSION,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
@@ -551,3 +553,16 @@ async def to_code(config: ConfigType) -> None:
 | 
			
		||||
            cg.add(dev.set_area_id(area_id_hash))
 | 
			
		||||
 | 
			
		||||
        cg.add(cg.App.register_device(dev))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Platform-specific source files for core
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "ring_buffer.cpp": {
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
 | 
			
		||||
        # as they are only included when needed by the preprocessor
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@
 | 
			
		||||
#define USE_DEEP_SLEEP
 | 
			
		||||
#define USE_DEVICES
 | 
			
		||||
#define USE_DISPLAY
 | 
			
		||||
#define USE_ENTITY_ICON
 | 
			
		||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
 | 
			
		||||
#define USE_EVENT
 | 
			
		||||
#define USE_FAN
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) {
 | 
			
		||||
 | 
			
		||||
// Entity Icon
 | 
			
		||||
std::string EntityBase::get_icon() const {
 | 
			
		||||
#ifdef USE_ENTITY_ICON
 | 
			
		||||
  if (this->icon_c_str_ == nullptr) {
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
  return this->icon_c_str_;
 | 
			
		||||
#else
 | 
			
		||||
  return "";
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void EntityBase::set_icon(const char *icon) {
 | 
			
		||||
#ifdef USE_ENTITY_ICON
 | 
			
		||||
  this->icon_c_str_ = icon;
 | 
			
		||||
#else
 | 
			
		||||
  // No-op when USE_ENTITY_ICON is not defined
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
 | 
			
		||||
 | 
			
		||||
// Entity Object ID
 | 
			
		||||
std::string EntityBase::get_object_id() const {
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,9 @@ class EntityBase {
 | 
			
		||||
 | 
			
		||||
  StringRef name_;
 | 
			
		||||
  const char *object_id_c_str_{nullptr};
 | 
			
		||||
#ifdef USE_ENTITY_ICON
 | 
			
		||||
  const char *icon_c_str_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t object_id_hash_{};
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
  Device *device_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_ID,
 | 
			
		||||
@@ -108,6 +109,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
 | 
			
		||||
    if CONF_INTERNAL in config:
 | 
			
		||||
        add(var.set_internal(config[CONF_INTERNAL]))
 | 
			
		||||
    if CONF_ICON in config:
 | 
			
		||||
        # Add USE_ENTITY_ICON define when icons are used
 | 
			
		||||
        cg.add_define("USE_ENTITY_ICON")
 | 
			
		||||
        add(var.set_icon(config[CONF_ICON]))
 | 
			
		||||
    if CONF_ENTITY_CATEGORY in config:
 | 
			
		||||
        add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
 | 
			
		||||
 
 | 
			
		||||
@@ -12,47 +12,10 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOST
 | 
			
		||||
#ifndef _WIN32
 | 
			
		||||
#include <net/if.h>
 | 
			
		||||
#include <netinet/in.h>
 | 
			
		||||
#include <sys/ioctl.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
#endif
 | 
			
		||||
#if defined(USE_ESP8266)
 | 
			
		||||
#include <osapi.h>
 | 
			
		||||
#include <user_interface.h>
 | 
			
		||||
// for xt_rsil()/xt_wsr_ps()
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
#elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
 | 
			
		||||
#include <Esp.h>
 | 
			
		||||
#elif defined(USE_ESP_IDF)
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/portmacro.h>
 | 
			
		||||
#include "esp_random.h"
 | 
			
		||||
#include "esp_system.h"
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
#if defined(USE_WIFI)
 | 
			
		||||
#include <WiFi.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include <hardware/structs/rosc.h>
 | 
			
		||||
#include <hardware/sync.h>
 | 
			
		||||
#elif defined(USE_HOST)
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <random>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include "esp_efuse.h"
 | 
			
		||||
#include "esp_efuse_table.h"
 | 
			
		||||
#include "esp_mac.h"
 | 
			
		||||
#include "rom/crc.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIBRETINY
 | 
			
		||||
#include <WiFi.h>  // for macAddress()
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "helpers";
 | 
			
		||||
@@ -177,70 +140,7 @@ uint32_t fnv1_hash(const std::string &str) {
 | 
			
		||||
  return hash;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
uint32_t random_uint32() { return esp_random(); }
 | 
			
		||||
#elif defined(USE_ESP8266)
 | 
			
		||||
uint32_t random_uint32() { return os_random(); }
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
uint32_t random_uint32() {
 | 
			
		||||
  uint32_t result = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < 32; i++) {
 | 
			
		||||
    result <<= 1;
 | 
			
		||||
    result |= rosc_hw->randombit;
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_LIBRETINY)
 | 
			
		||||
uint32_t random_uint32() { return rand(); }
 | 
			
		||||
#elif defined(USE_HOST)
 | 
			
		||||
uint32_t random_uint32() {
 | 
			
		||||
  std::random_device dev;
 | 
			
		||||
  std::mt19937 rng(dev());
 | 
			
		||||
  std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max());
 | 
			
		||||
  return dist(rng);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); }
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  esp_fill_random(data, len);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_ESP8266)
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  while (len-- != 0) {
 | 
			
		||||
    uint8_t result = 0;
 | 
			
		||||
    for (uint8_t i = 0; i < 8; i++) {
 | 
			
		||||
      result <<= 1;
 | 
			
		||||
      result |= rosc_hw->randombit;
 | 
			
		||||
    }
 | 
			
		||||
    *data++ = result;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_LIBRETINY)
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  lt_rand_bytes(data, len);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_HOST)
 | 
			
		||||
bool random_bytes(uint8_t *data, size_t len) {
 | 
			
		||||
  FILE *fp = fopen("/dev/urandom", "r");
 | 
			
		||||
  if (fp == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
 | 
			
		||||
    exit(1);
 | 
			
		||||
  }
 | 
			
		||||
  size_t read = fread(data, 1, len, fp);
 | 
			
		||||
  if (read != len) {
 | 
			
		||||
    ESP_LOGW(TAG, "Not enough data from /dev/urandom");
 | 
			
		||||
    exit(1);
 | 
			
		||||
  }
 | 
			
		||||
  fclose(fp);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Strings
 | 
			
		||||
 | 
			
		||||
@@ -460,9 +360,22 @@ int8_t step_to_accuracy_decimals(float step) {
 | 
			
		||||
  return str.length() - dot_pos - 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 | 
			
		||||
                                        "abcdefghijklmnopqrstuvwxyz"
 | 
			
		||||
                                        "0123456789+/";
 | 
			
		||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
 | 
			
		||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 | 
			
		||||
                                            "abcdefghijklmnopqrstuvwxyz"
 | 
			
		||||
                                            "0123456789+/";
 | 
			
		||||
 | 
			
		||||
// Helper function to find the index of a base64 character in the lookup table.
 | 
			
		||||
// Returns the character's position (0-63) if found, or 0 if not found.
 | 
			
		||||
// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters.
 | 
			
		||||
// This is safe because is_base64() is ALWAYS checked before calling this function,
 | 
			
		||||
// preventing invalid characters from ever reaching here. The base64_decode function
 | 
			
		||||
// stops processing at the first invalid character due to the is_base64() check in its
 | 
			
		||||
// while loop condition, making this edge case harmless in practice.
 | 
			
		||||
static inline uint8_t base64_find_char(char c) {
 | 
			
		||||
  const char *pos = strchr(BASE64_CHARS, c);
 | 
			
		||||
  return pos ? (pos - BASE64_CHARS) : 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
 | 
			
		||||
 | 
			
		||||
@@ -484,7 +397,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
 | 
			
		||||
      char_array_4[3] = char_array_3[2] & 0x3f;
 | 
			
		||||
 | 
			
		||||
      for (i = 0; (i < 4); i++)
 | 
			
		||||
        ret += BASE64_CHARS[char_array_4[i]];
 | 
			
		||||
        ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[i])];
 | 
			
		||||
      i = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -499,7 +412,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
 | 
			
		||||
    char_array_4[3] = char_array_3[2] & 0x3f;
 | 
			
		||||
 | 
			
		||||
    for (j = 0; (j < i + 1); j++)
 | 
			
		||||
      ret += BASE64_CHARS[char_array_4[j]];
 | 
			
		||||
      ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
 | 
			
		||||
 | 
			
		||||
    while ((i++ < 3))
 | 
			
		||||
      ret += '=';
 | 
			
		||||
@@ -526,12 +439,15 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
 | 
			
		||||
  uint8_t char_array_4[4], char_array_3[3];
 | 
			
		||||
  std::vector<uint8_t> ret;
 | 
			
		||||
 | 
			
		||||
  // SAFETY: The loop condition checks is_base64() before processing each character.
 | 
			
		||||
  // This ensures base64_find_char() is only called on valid base64 characters,
 | 
			
		||||
  // preventing the edge case where invalid chars would return 0 (same as 'A').
 | 
			
		||||
  while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) {
 | 
			
		||||
    char_array_4[i++] = encoded_string[in];
 | 
			
		||||
    in++;
 | 
			
		||||
    if (i == 4) {
 | 
			
		||||
      for (i = 0; i < 4; i++)
 | 
			
		||||
        char_array_4[i] = BASE64_CHARS.find(char_array_4[i]);
 | 
			
		||||
        char_array_4[i] = base64_find_char(char_array_4[i]);
 | 
			
		||||
 | 
			
		||||
      char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
 | 
			
		||||
      char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
 | 
			
		||||
@@ -548,7 +464,7 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
 | 
			
		||||
      char_array_4[j] = 0;
 | 
			
		||||
 | 
			
		||||
    for (j = 0; j < 4; j++)
 | 
			
		||||
      char_array_4[j] = BASE64_CHARS.find(char_array_4[j]);
 | 
			
		||||
      char_array_4[j] = base64_find_char(char_array_4[j]);
 | 
			
		||||
 | 
			
		||||
    char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
 | 
			
		||||
    char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
 | 
			
		||||
@@ -644,42 +560,6 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
 | 
			
		||||
  blue += delta;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// System APIs
 | 
			
		||||
#if defined(USE_ESP8266) || defined(USE_RP2040)
 | 
			
		||||
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
 | 
			
		||||
Mutex::Mutex() {}
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() {}
 | 
			
		||||
bool Mutex::try_lock() { return true; }
 | 
			
		||||
void Mutex::unlock() {}
 | 
			
		||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
			
		||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
 | 
			
		||||
Mutex::~Mutex() {}
 | 
			
		||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
 | 
			
		||||
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
 | 
			
		||||
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
 | 
			
		||||
#elif defined(USE_HOST)
 | 
			
		||||
// Host platform uses std::mutex for proper thread synchronization
 | 
			
		||||
Mutex::Mutex() { handle_ = new std::mutex(); }
 | 
			
		||||
Mutex::~Mutex() { delete static_cast<std::mutex *>(handle_); }
 | 
			
		||||
void Mutex::lock() { static_cast<std::mutex *>(handle_)->lock(); }
 | 
			
		||||
bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock(); }
 | 
			
		||||
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP8266)
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
 | 
			
		||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
			
		||||
// only affects the executing core
 | 
			
		||||
// so should not be used as a mutex lock, only to get accurate timing
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
 | 
			
		||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
uint8_t HighFrequencyLoopRequester::num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
void HighFrequencyLoopRequester::start() {
 | 
			
		||||
  if (this->started_)
 | 
			
		||||
@@ -695,45 +575,6 @@ void HighFrequencyLoopRequester::stop() {
 | 
			
		||||
}
 | 
			
		||||
bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; }
 | 
			
		||||
 | 
			
		||||
#if defined(USE_HOST)
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
 | 
			
		||||
  memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_ESP32)
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
 | 
			
		||||
  // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
 | 
			
		||||
  // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
 | 
			
		||||
  if (has_custom_mac_address()) {
 | 
			
		||||
    esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
 | 
			
		||||
  } else {
 | 
			
		||||
    esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  if (has_custom_mac_address()) {
 | 
			
		||||
    esp_efuse_mac_get_custom(mac);
 | 
			
		||||
  } else {
 | 
			
		||||
    esp_efuse_mac_get_default(mac);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_ESP8266)
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  wifi_get_macaddr(STATION_IF, mac);
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
  WiFi.macAddress(mac);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#elif defined(USE_LIBRETINY)
 | 
			
		||||
void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 | 
			
		||||
  WiFi.macAddress(mac);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
std::string get_mac_address() {
 | 
			
		||||
  uint8_t mac[6];
 | 
			
		||||
  get_mac_address_raw(mac);
 | 
			
		||||
@@ -746,24 +587,10 @@ std::string get_mac_address_pretty() {
 | 
			
		||||
  return format_mac_address_pretty(mac);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
 | 
			
		||||
#ifndef USE_ESP32
 | 
			
		||||
bool has_custom_mac_address() { return false; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool has_custom_mac_address() {
 | 
			
		||||
#if defined(USE_ESP32) && !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
 | 
			
		||||
  uint8_t mac[6];
 | 
			
		||||
  // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
 | 
			
		||||
#ifndef USE_ESP32_VARIANT_ESP32
 | 
			
		||||
  return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
 | 
			
		||||
#else
 | 
			
		||||
  return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
 | 
			
		||||
#endif
 | 
			
		||||
#else
 | 
			
		||||
  return false;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool mac_address_is_valid(const uint8_t *mac) {
 | 
			
		||||
  bool is_all_zeros = true;
 | 
			
		||||
  bool is_all_ones = true;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								esphome/core/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								esphome/core/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
#include "esphome/core/runtime_stats.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
RuntimeStatsCollector runtime_stats;
 | 
			
		||||
 | 
			
		||||
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
 | 
			
		||||
  if (!this->enabled_ || component == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  // Check if we have cached the name for this component
 | 
			
		||||
  auto name_it = this->component_names_cache_.find(component);
 | 
			
		||||
  if (name_it == this->component_names_cache_.end()) {
 | 
			
		||||
    // First time seeing this component, cache its name
 | 
			
		||||
    const char *source = component->get_component_source();
 | 
			
		||||
    this->component_names_cache_[component] = source;
 | 
			
		||||
    this->component_stats_[source].record_time(duration_ms);
 | 
			
		||||
  } else {
 | 
			
		||||
    // Use cached name - no string operations, just map lookup
 | 
			
		||||
    this->component_stats_[name_it->second].record_time(duration_ms);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If next_log_time_ is 0, initialize it
 | 
			
		||||
  if (this->next_log_time_ == 0) {
 | 
			
		||||
    this->next_log_time_ = current_time + this->log_interval_;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Don't print stats here anymore - let process_pending_stats handle it
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RuntimeStatsCollector::log_stats_() {
 | 
			
		||||
  ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
 | 
			
		||||
  ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
 | 
			
		||||
 | 
			
		||||
  // First collect stats we want to display
 | 
			
		||||
  std::vector<ComponentStatPair> stats_to_display;
 | 
			
		||||
 | 
			
		||||
  for (const auto &it : this->component_stats_) {
 | 
			
		||||
    const ComponentRuntimeStats &stats = it.second;
 | 
			
		||||
    if (stats.get_period_count() > 0) {
 | 
			
		||||
      ComponentStatPair pair = {it.first, &stats};
 | 
			
		||||
      stats_to_display.push_back(pair);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Sort by period runtime (descending)
 | 
			
		||||
  std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
 | 
			
		||||
 | 
			
		||||
  // Log top components by period runtime
 | 
			
		||||
  for (const auto &it : stats_to_display) {
 | 
			
		||||
    const std::string &source = it.name;
 | 
			
		||||
    const ComponentRuntimeStats *stats = it.stats;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(RUNTIME_TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
 | 
			
		||||
             stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
 | 
			
		||||
             stats->get_period_time_ms());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Log total stats since boot
 | 
			
		||||
  ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
 | 
			
		||||
 | 
			
		||||
  // Re-sort by total runtime for all-time stats
 | 
			
		||||
  std::sort(stats_to_display.begin(), stats_to_display.end(),
 | 
			
		||||
            [](const ComponentStatPair &a, const ComponentStatPair &b) {
 | 
			
		||||
              return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
  for (const auto &it : stats_to_display) {
 | 
			
		||||
    const std::string &source = it.name;
 | 
			
		||||
    const ComponentRuntimeStats *stats = it.stats;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(RUNTIME_TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
 | 
			
		||||
             stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
 | 
			
		||||
             stats->get_total_time_ms());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
 | 
			
		||||
  if (!this->enabled_ || this->next_log_time_ == 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  if (current_time >= this->next_log_time_) {
 | 
			
		||||
    this->log_stats_();
 | 
			
		||||
    this->reset_stats_();
 | 
			
		||||
    this->next_log_time_ = current_time + this->log_interval_;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										121
									
								
								esphome/core/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/core/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
static const char *const RUNTIME_TAG = "runtime";
 | 
			
		||||
 | 
			
		||||
class Component;  // Forward declaration
 | 
			
		||||
 | 
			
		||||
class ComponentRuntimeStats {
 | 
			
		||||
 public:
 | 
			
		||||
  ComponentRuntimeStats()
 | 
			
		||||
      : period_count_(0),
 | 
			
		||||
        total_count_(0),
 | 
			
		||||
        period_time_ms_(0),
 | 
			
		||||
        total_time_ms_(0),
 | 
			
		||||
        period_max_time_ms_(0),
 | 
			
		||||
        total_max_time_ms_(0) {}
 | 
			
		||||
 | 
			
		||||
  void record_time(uint32_t duration_ms) {
 | 
			
		||||
    // Update period counters
 | 
			
		||||
    this->period_count_++;
 | 
			
		||||
    this->period_time_ms_ += duration_ms;
 | 
			
		||||
    if (duration_ms > this->period_max_time_ms_)
 | 
			
		||||
      this->period_max_time_ms_ = duration_ms;
 | 
			
		||||
 | 
			
		||||
    // Update total counters
 | 
			
		||||
    this->total_count_++;
 | 
			
		||||
    this->total_time_ms_ += duration_ms;
 | 
			
		||||
    if (duration_ms > this->total_max_time_ms_)
 | 
			
		||||
      this->total_max_time_ms_ = duration_ms;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void reset_period_stats() {
 | 
			
		||||
    this->period_count_ = 0;
 | 
			
		||||
    this->period_time_ms_ = 0;
 | 
			
		||||
    this->period_max_time_ms_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Period stats (reset each logging interval)
 | 
			
		||||
  uint32_t get_period_count() const { return this->period_count_; }
 | 
			
		||||
  uint32_t get_period_time_ms() const { return this->period_time_ms_; }
 | 
			
		||||
  uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
 | 
			
		||||
  float get_period_avg_time_ms() const {
 | 
			
		||||
    return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Total stats (persistent until reboot)
 | 
			
		||||
  uint32_t get_total_count() const { return this->total_count_; }
 | 
			
		||||
  uint32_t get_total_time_ms() const { return this->total_time_ms_; }
 | 
			
		||||
  uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
 | 
			
		||||
  float get_total_avg_time_ms() const {
 | 
			
		||||
    return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Period stats (reset each logging interval)
 | 
			
		||||
  uint32_t period_count_;
 | 
			
		||||
  uint32_t period_time_ms_;
 | 
			
		||||
  uint32_t period_max_time_ms_;
 | 
			
		||||
 | 
			
		||||
  // Total stats (persistent until reboot)
 | 
			
		||||
  uint32_t total_count_;
 | 
			
		||||
  uint32_t total_time_ms_;
 | 
			
		||||
  uint32_t total_max_time_ms_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// For sorting components by run time
 | 
			
		||||
struct ComponentStatPair {
 | 
			
		||||
  std::string name;
 | 
			
		||||
  const ComponentRuntimeStats *stats;
 | 
			
		||||
 | 
			
		||||
  bool operator>(const ComponentStatPair &other) const {
 | 
			
		||||
    // Sort by period time as that's what we're displaying in the logs
 | 
			
		||||
    return stats->get_period_time_ms() > other.stats->get_period_time_ms();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class RuntimeStatsCollector {
 | 
			
		||||
 public:
 | 
			
		||||
  RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
 | 
			
		||||
 | 
			
		||||
  void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
 | 
			
		||||
  uint32_t get_log_interval() const { return this->log_interval_; }
 | 
			
		||||
 | 
			
		||||
  void set_enabled(bool enabled) { this->enabled_ = enabled; }
 | 
			
		||||
  bool is_enabled() const { return this->enabled_; }
 | 
			
		||||
 | 
			
		||||
  void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
 | 
			
		||||
 | 
			
		||||
  // Process any pending stats printing (should be called after component loop)
 | 
			
		||||
  void process_pending_stats(uint32_t current_time);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void log_stats_();
 | 
			
		||||
 | 
			
		||||
  void reset_stats_() {
 | 
			
		||||
    for (auto &it : this->component_stats_) {
 | 
			
		||||
      it.second.reset_period_stats();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Back to string keys, but we'll cache the source name per component
 | 
			
		||||
  std::map<std::string, ComponentRuntimeStats> component_stats_;
 | 
			
		||||
  std::map<Component *, std::string> component_names_cache_;
 | 
			
		||||
  uint32_t log_interval_;
 | 
			
		||||
  uint32_t next_log_time_;
 | 
			
		||||
  bool enabled_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Global instance for runtime stats collection
 | 
			
		||||
extern RuntimeStatsCollector runtime_stats;
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -62,16 +62,16 @@ static void validate_static_string(const char *name) {
 | 
			
		||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
 | 
			
		||||
                                      const void *name_ptr, uint32_t delay, std::function<void()> func) {
 | 
			
		||||
  // Get the name as const char*
 | 
			
		||||
  const char *name_cstr =
 | 
			
		||||
      is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
 | 
			
		||||
  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 | 
			
		||||
 | 
			
		||||
  // Cancel existing timer if name is not empty
 | 
			
		||||
  if (name_cstr != nullptr && name_cstr[0] != '\0') {
 | 
			
		||||
    this->cancel_item_(component, name_cstr, type);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (delay == SCHEDULER_DONT_RUN)
 | 
			
		||||
  if (delay == SCHEDULER_DONT_RUN) {
 | 
			
		||||
    // Still need to cancel existing timer if name is not empty
 | 
			
		||||
    if (this->is_name_valid_(name_cstr)) {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Create and populate the scheduler item
 | 
			
		||||
  auto item = make_unique<SchedulerItem>();
 | 
			
		||||
@@ -87,6 +87,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
 | 
			
		||||
  if (delay == 0 && type == SchedulerItem::TIMEOUT) {
 | 
			
		||||
    // Put in defer queue for guaranteed FIFO execution
 | 
			
		||||
    LockGuard guard{this->lock_};
 | 
			
		||||
    this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
    this->defer_queue_.push_back(std::move(item));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -122,7 +123,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->push_(std::move(item));
 | 
			
		||||
  LockGuard guard{this->lock_};
 | 
			
		||||
  // If name is provided, do atomic cancel-and-add
 | 
			
		||||
  if (this->is_name_valid_(name_cstr)) {
 | 
			
		||||
    // Cancel existing items
 | 
			
		||||
    this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
  }
 | 
			
		||||
  // Add new item directly to to_add_
 | 
			
		||||
  // since we have the lock held
 | 
			
		||||
  this->to_add_.push_back(std::move(item));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) {
 | 
			
		||||
@@ -134,10 +143,10 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u
 | 
			
		||||
  this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func));
 | 
			
		||||
}
 | 
			
		||||
bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
 | 
			
		||||
  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
 | 
			
		||||
  return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT);
 | 
			
		||||
}
 | 
			
		||||
bool HOT Scheduler::cancel_timeout(Component *component, const char *name) {
 | 
			
		||||
  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
 | 
			
		||||
  return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT);
 | 
			
		||||
}
 | 
			
		||||
void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
 | 
			
		||||
                                 std::function<void()> func) {
 | 
			
		||||
@@ -149,10 +158,10 @@ void HOT Scheduler::set_interval(Component *component, const char *name, uint32_
 | 
			
		||||
  this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func));
 | 
			
		||||
}
 | 
			
		||||
bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
 | 
			
		||||
  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
 | 
			
		||||
  return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL);
 | 
			
		||||
}
 | 
			
		||||
bool HOT Scheduler::cancel_interval(Component *component, const char *name) {
 | 
			
		||||
  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
 | 
			
		||||
  return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct RetryArgs {
 | 
			
		||||
@@ -211,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<uint32_t> HOT Scheduler::next_schedule_in() {
 | 
			
		||||
  // IMPORTANT: This method should only be called from the main thread (loop task).
 | 
			
		||||
  // It calls empty_() and accesses items_[0] without holding a lock, which is only
 | 
			
		||||
  // safe when called from the main thread. Other threads must not call this method.
 | 
			
		||||
  if (this->empty_())
 | 
			
		||||
    return {};
 | 
			
		||||
  auto &item = this->items_[0];
 | 
			
		||||
@@ -230,6 +242,10 @@ void HOT Scheduler::call() {
 | 
			
		||||
  // - No deferred items exist in to_add_, so processing order doesn't affect correctness
 | 
			
		||||
  // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
 | 
			
		||||
  // (ESP8266: single-core, RP2040: empty mutex implementation).
 | 
			
		||||
  //
 | 
			
		||||
  // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
 | 
			
		||||
  // processed here. They are removed from the queue normally via pop_front() but skipped
 | 
			
		||||
  // during execution by should_skip_item_(). This is intentional - no memory leak occurs.
 | 
			
		||||
  while (!this->defer_queue_.empty()) {
 | 
			
		||||
    // The outer check is done without a lock for performance. If the queue
 | 
			
		||||
    // appears non-empty, we lock and process an item. We don't need to check
 | 
			
		||||
@@ -261,10 +277,12 @@ void HOT Scheduler::call() {
 | 
			
		||||
    ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
 | 
			
		||||
             this->last_millis_);
 | 
			
		||||
    while (!this->empty_()) {
 | 
			
		||||
      this->lock_.lock();
 | 
			
		||||
      auto item = std::move(this->items_[0]);
 | 
			
		||||
      this->pop_raw_();
 | 
			
		||||
      this->lock_.unlock();
 | 
			
		||||
      std::unique_ptr<SchedulerItem> item;
 | 
			
		||||
      {
 | 
			
		||||
        LockGuard guard{this->lock_};
 | 
			
		||||
        item = std::move(this->items_[0]);
 | 
			
		||||
        this->pop_raw_();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const char *name = item->get_name();
 | 
			
		||||
      ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
 | 
			
		||||
@@ -278,33 +296,35 @@ void HOT Scheduler::call() {
 | 
			
		||||
    {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      this->items_ = std::move(old_items);
 | 
			
		||||
      // Rebuild heap after moving items back
 | 
			
		||||
      std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif  // ESPHOME_DEBUG_SCHEDULER
 | 
			
		||||
 | 
			
		||||
  auto to_remove_was = to_remove_;
 | 
			
		||||
  auto items_was = this->items_.size();
 | 
			
		||||
  // If we have too many items to remove
 | 
			
		||||
  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
 | 
			
		||||
  if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
 | 
			
		||||
    // We hold the lock for the entire cleanup operation because:
 | 
			
		||||
    // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
 | 
			
		||||
    // 2. Other threads must see either the old state or the new state, not intermediate states
 | 
			
		||||
    // 3. The operation is already expensive (O(n)), so lock overhead is negligible
 | 
			
		||||
    // 4. No operations inside can block or take other locks, so no deadlock risk
 | 
			
		||||
    LockGuard guard{this->lock_};
 | 
			
		||||
 | 
			
		||||
    std::vector<std::unique_ptr<SchedulerItem>> valid_items;
 | 
			
		||||
    while (!this->empty_()) {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      auto item = std::move(this->items_[0]);
 | 
			
		||||
      this->pop_raw_();
 | 
			
		||||
      valid_items.push_back(std::move(item));
 | 
			
		||||
 | 
			
		||||
    // Move all non-removed items to valid_items
 | 
			
		||||
    for (auto &item : this->items_) {
 | 
			
		||||
      if (!item->remove) {
 | 
			
		||||
        valid_items.push_back(std::move(item));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      this->items_ = std::move(valid_items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The following should not happen unless I'm missing something
 | 
			
		||||
    if (to_remove_ != 0) {
 | 
			
		||||
      ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this",
 | 
			
		||||
               to_remove_was, to_remove_, items_was, items_.size());
 | 
			
		||||
      to_remove_ = 0;
 | 
			
		||||
    }
 | 
			
		||||
    // Replace items_ with the filtered list
 | 
			
		||||
    this->items_ = std::move(valid_items);
 | 
			
		||||
    // Rebuild the heap structure since items are no longer in heap order
 | 
			
		||||
    std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
 | 
			
		||||
    this->to_remove_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  while (!this->empty_()) {
 | 
			
		||||
@@ -336,26 +356,25 @@ void HOT Scheduler::call() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      this->lock_.lock();
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
 | 
			
		||||
      // new scope, item from before might have been moved in the vector
 | 
			
		||||
      auto item = std::move(this->items_[0]);
 | 
			
		||||
 | 
			
		||||
      // Only pop after function call, this ensures we were reachable
 | 
			
		||||
      // during the function call and know if we were cancelled.
 | 
			
		||||
      this->pop_raw_();
 | 
			
		||||
 | 
			
		||||
      this->lock_.unlock();
 | 
			
		||||
 | 
			
		||||
      if (item->remove) {
 | 
			
		||||
        // We were removed/cancelled in the function call, stop
 | 
			
		||||
        to_remove_--;
 | 
			
		||||
        this->to_remove_--;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (item->type == SchedulerItem::INTERVAL) {
 | 
			
		||||
        item->next_execution_ = now + item->interval;
 | 
			
		||||
        this->push_(std::move(item));
 | 
			
		||||
        // Add new item directly to to_add_
 | 
			
		||||
        // since we have the lock held
 | 
			
		||||
        this->to_add_.push_back(std::move(item));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -375,36 +394,37 @@ void HOT Scheduler::process_to_add() {
 | 
			
		||||
  this->to_add_.clear();
 | 
			
		||||
}
 | 
			
		||||
void HOT Scheduler::cleanup_() {
 | 
			
		||||
  // Fast path: if nothing to remove, just return
 | 
			
		||||
  // Reading to_remove_ without lock is safe because:
 | 
			
		||||
  // 1. We only call this from the main thread during call()
 | 
			
		||||
  // 2. If it's 0, there's definitely nothing to cleanup
 | 
			
		||||
  // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration
 | 
			
		||||
  // 4. Not all platforms support atomics, so we accept this race in favor of performance
 | 
			
		||||
  // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless
 | 
			
		||||
  if (this->to_remove_ == 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  // We must hold the lock for the entire cleanup operation because:
 | 
			
		||||
  // 1. We're modifying items_ (via pop_raw_) which requires exclusive access
 | 
			
		||||
  // 2. We're decrementing to_remove_ which is also modified by other threads
 | 
			
		||||
  //    (though all modifications are already under lock)
 | 
			
		||||
  // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_()
 | 
			
		||||
  // 4. We need a consistent view of items_ and to_remove_ throughout the operation
 | 
			
		||||
  // Without the lock, we could access items_ while another thread is reading it,
 | 
			
		||||
  // leading to race conditions
 | 
			
		||||
  LockGuard guard{this->lock_};
 | 
			
		||||
  while (!this->items_.empty()) {
 | 
			
		||||
    auto &item = this->items_[0];
 | 
			
		||||
    if (!item->remove)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    to_remove_--;
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      LockGuard guard{this->lock_};
 | 
			
		||||
      this->pop_raw_();
 | 
			
		||||
    }
 | 
			
		||||
    this->to_remove_--;
 | 
			
		||||
    this->pop_raw_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HOT Scheduler::pop_raw_() {
 | 
			
		||||
  std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
 | 
			
		||||
  this->items_.pop_back();
 | 
			
		||||
}
 | 
			
		||||
void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) {
 | 
			
		||||
  LockGuard guard{this->lock_};
 | 
			
		||||
  this->to_add_.push_back(std::move(item));
 | 
			
		||||
}
 | 
			
		||||
// Helper function to check if item matches criteria for cancellation
 | 
			
		||||
bool HOT Scheduler::matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component,
 | 
			
		||||
                                  const char *name_cstr, SchedulerItem::Type type) {
 | 
			
		||||
  if (item->component != component || item->type != type || item->remove) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  const char *item_name = item->get_name();
 | 
			
		||||
  return item_name != nullptr && strcmp(name_cstr, item_name) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to execute a scheduler item
 | 
			
		||||
void HOT Scheduler::execute_item_(SchedulerItem *item) {
 | 
			
		||||
@@ -417,55 +437,56 @@ void HOT Scheduler::execute_item_(SchedulerItem *item) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for cancel operations
 | 
			
		||||
bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr,
 | 
			
		||||
                                        SchedulerItem::Type type) {
 | 
			
		||||
bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr,
 | 
			
		||||
                                 SchedulerItem::Type type) {
 | 
			
		||||
  // Get the name as const char*
 | 
			
		||||
  const char *name_cstr =
 | 
			
		||||
      is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
 | 
			
		||||
  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 | 
			
		||||
 | 
			
		||||
  // Handle null or empty names
 | 
			
		||||
  if (name_cstr == nullptr)
 | 
			
		||||
  if (!this->is_name_valid_(name_cstr))
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  // obtain lock because this function iterates and can be called from non-loop task context
 | 
			
		||||
  LockGuard guard{this->lock_};
 | 
			
		||||
  bool ret = false;
 | 
			
		||||
  return this->cancel_item_locked_(component, name_cstr, type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to cancel items by name - must be called with lock held
 | 
			
		||||
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
 | 
			
		||||
  size_t total_cancelled = 0;
 | 
			
		||||
 | 
			
		||||
  // Check all containers for matching items
 | 
			
		||||
#if !defined(USE_ESP8266) && !defined(USE_RP2040)
 | 
			
		||||
  // Only check defer_queue_ on platforms that have it
 | 
			
		||||
  for (auto &item : this->defer_queue_) {
 | 
			
		||||
    if (this->matches_item_(item, component, name_cstr, type)) {
 | 
			
		||||
      item->remove = true;
 | 
			
		||||
      ret = true;
 | 
			
		||||
  // Only check defer queue for timeouts (intervals never go there)
 | 
			
		||||
  if (type == SchedulerItem::TIMEOUT) {
 | 
			
		||||
    for (auto &item : this->defer_queue_) {
 | 
			
		||||
      if (this->matches_item_(item, component, name_cstr, type)) {
 | 
			
		||||
        item->remove = true;
 | 
			
		||||
        total_cancelled++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Cancel items in the main heap
 | 
			
		||||
  for (auto &item : this->items_) {
 | 
			
		||||
    if (this->matches_item_(item, component, name_cstr, type)) {
 | 
			
		||||
      item->remove = true;
 | 
			
		||||
      ret = true;
 | 
			
		||||
      this->to_remove_++;  // Only track removals for heap items
 | 
			
		||||
      total_cancelled++;
 | 
			
		||||
      this->to_remove_++;  // Track removals for heap items
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Cancel items in to_add_
 | 
			
		||||
  for (auto &item : this->to_add_) {
 | 
			
		||||
    if (this->matches_item_(item, component, name_cstr, type)) {
 | 
			
		||||
      item->remove = true;
 | 
			
		||||
      ret = true;
 | 
			
		||||
      total_cancelled++;
 | 
			
		||||
      // Don't track removals for to_add_ items
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
 | 
			
		||||
  return this->cancel_item_common_(component, false, &name, type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) {
 | 
			
		||||
  return this->cancel_item_common_(component, true, name, type);
 | 
			
		||||
  return total_cancelled > 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint64_t Scheduler::millis_() {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <deque>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
@@ -98,9 +99,9 @@ class Scheduler {
 | 
			
		||||
    SchedulerItem(const SchedulerItem &) = delete;
 | 
			
		||||
    SchedulerItem &operator=(const SchedulerItem &) = delete;
 | 
			
		||||
 | 
			
		||||
    // Default move operations
 | 
			
		||||
    SchedulerItem(SchedulerItem &&) = default;
 | 
			
		||||
    SchedulerItem &operator=(SchedulerItem &&) = default;
 | 
			
		||||
    // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
 | 
			
		||||
    SchedulerItem(SchedulerItem &&) = delete;
 | 
			
		||||
    SchedulerItem &operator=(SchedulerItem &&) = delete;
 | 
			
		||||
 | 
			
		||||
    // Helper to get the name regardless of storage type
 | 
			
		||||
    const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
 | 
			
		||||
@@ -139,17 +140,42 @@ class Scheduler {
 | 
			
		||||
  uint64_t millis_();
 | 
			
		||||
  void cleanup_();
 | 
			
		||||
  void pop_raw_();
 | 
			
		||||
  void push_(std::unique_ptr<SchedulerItem> item);
 | 
			
		||||
  // Common implementation for cancel operations
 | 
			
		||||
  bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
 | 
			
		||||
  bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type);
 | 
			
		||||
  // Helper to cancel items by name - must be called with lock held
 | 
			
		||||
  bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type);
 | 
			
		||||
 | 
			
		||||
  // Helper functions for cancel operations
 | 
			
		||||
  bool matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
 | 
			
		||||
                     SchedulerItem::Type type);
 | 
			
		||||
  // Helper to extract name as const char* from either static string or std::string
 | 
			
		||||
  inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
 | 
			
		||||
    return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to check if a name is valid (not null and not empty)
 | 
			
		||||
  inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; }
 | 
			
		||||
 | 
			
		||||
  // Common implementation for cancel operations
 | 
			
		||||
  bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
 | 
			
		||||
 | 
			
		||||
  // Helper function to check if item matches criteria for cancellation
 | 
			
		||||
  inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
 | 
			
		||||
                                SchedulerItem::Type type) {
 | 
			
		||||
    if (item->component != component || item->type != type || item->remove) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const char *item_name = item->get_name();
 | 
			
		||||
    if (item_name == nullptr) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    // Fast path: if pointers are equal
 | 
			
		||||
    // This is effective because the core ESPHome codebase uses static strings (const char*)
 | 
			
		||||
    // for component names. The std::string overloads exist only for compatibility with
 | 
			
		||||
    // external components, but are rarely used in practice.
 | 
			
		||||
    if (item_name == name_cstr) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    // Slow path: compare string contents
 | 
			
		||||
    return strcmp(name_cstr, item_name) == 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to execute a scheduler item
 | 
			
		||||
  void execute_item_(SchedulerItem *item);
 | 
			
		||||
@@ -159,6 +185,12 @@ class Scheduler {
 | 
			
		||||
    return item->remove || (item->component != nullptr && item->component->is_failed());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check if the scheduler has no items.
 | 
			
		||||
  // IMPORTANT: This method should only be called from the main thread (loop task).
 | 
			
		||||
  // It performs cleanup of removed items and checks if the queue is empty.
 | 
			
		||||
  // The items_.empty() check at the end is done without a lock for performance,
 | 
			
		||||
  // which is safe because this is only called from the main thread while other
 | 
			
		||||
  // threads only add items (never remove them).
 | 
			
		||||
  bool empty_() {
 | 
			
		||||
    this->cleanup_();
 | 
			
		||||
    return this->items_.empty();
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import os
 | 
			
		||||
from typing import TYPE_CHECKING, Any
 | 
			
		||||
 | 
			
		||||
from esphome import const, util
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
from esphome.storage_json import StorageJSON, ext_storage_path
 | 
			
		||||
 | 
			
		||||
from .const import (
 | 
			
		||||
@@ -18,7 +19,6 @@ from .const import (
 | 
			
		||||
    EVENT_ENTRY_STATE_CHANGED,
 | 
			
		||||
    EVENT_ENTRY_UPDATED,
 | 
			
		||||
)
 | 
			
		||||
from .enum import StrEnum
 | 
			
		||||
from .util.subprocess import async_run_system_command
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
 
 | 
			
		||||
@@ -112,8 +112,17 @@ class ComponentManifest:
 | 
			
		||||
        This will return all cpp source files that are located in the same folder as the
 | 
			
		||||
        loaded .py file (does not look through subdirectories)
 | 
			
		||||
        """
 | 
			
		||||
        ret = []
 | 
			
		||||
        ret: list[FileResource] = []
 | 
			
		||||
 | 
			
		||||
        # Get filter function for source files
 | 
			
		||||
        filter_source_files_func = getattr(self.module, "FILTER_SOURCE_FILES", None)
 | 
			
		||||
 | 
			
		||||
        # Get list of files to exclude
 | 
			
		||||
        excluded_files = (
 | 
			
		||||
            set(filter_source_files_func()) if filter_source_files_func else set()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Process all resources
 | 
			
		||||
        for resource in (
 | 
			
		||||
            r.name
 | 
			
		||||
            for r in importlib.resources.files(self.package).iterdir()
 | 
			
		||||
@@ -124,6 +133,11 @@ class ComponentManifest:
 | 
			
		||||
            if not importlib.resources.files(self.package).joinpath(resource).is_file():
 | 
			
		||||
                # Not a resource = this is a directory (yeah this is confusing)
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # Skip excluded files
 | 
			
		||||
            if resource in excluded_files:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            ret.append(FileResource(self.package, resource))
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -290,7 +290,7 @@ class DoubleType(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED64  # Uses wire type 1 according to protobuf spec
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%g", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -312,7 +312,7 @@ class FloatType(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED32  # Uses wire type 5
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%g", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -334,7 +334,7 @@ class Int64Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%lld", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -356,7 +356,7 @@ class UInt64Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%llu", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -378,7 +378,7 @@ class Int32Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%" PRId32, {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -400,7 +400,7 @@ class Fixed64Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED64  # Uses wire type 1
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%llu", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -422,7 +422,7 @@ class Fixed32Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED32  # Uses wire type 5
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -555,7 +555,7 @@ class UInt32Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -607,7 +607,7 @@ class SFixed32Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED32  # Uses wire type 5
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%" PRId32, {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -629,7 +629,7 @@ class SFixed64Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.FIXED64  # Uses wire type 1
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%lld", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -651,7 +651,7 @@ class SInt32Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%" PRId32, {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
@@ -673,7 +673,7 @@ class SInt64Type(TypeInfo):
 | 
			
		||||
    wire_type = WireType.VARINT  # Uses wire type 0
 | 
			
		||||
 | 
			
		||||
    def dump(self, name: str) -> str:
 | 
			
		||||
        o = f'sprintf(buffer, "%lld", {name});\n'
 | 
			
		||||
        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
 | 
			
		||||
        o += "out.append(buffer);"
 | 
			
		||||
        return o
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -559,6 +559,12 @@ def lint_relative_py_import(fname):
 | 
			
		||||
        "esphome/components/libretiny/core.cpp",
 | 
			
		||||
        "esphome/components/host/core.cpp",
 | 
			
		||||
        "esphome/components/zephyr/core.cpp",
 | 
			
		||||
        "esphome/components/esp32/helpers.cpp",
 | 
			
		||||
        "esphome/components/esp8266/helpers.cpp",
 | 
			
		||||
        "esphome/components/rp2040/helpers.cpp",
 | 
			
		||||
        "esphome/components/libretiny/helpers.cpp",
 | 
			
		||||
        "esphome/components/host/helpers.cpp",
 | 
			
		||||
        "esphome/components/zephyr/helpers.cpp",
 | 
			
		||||
        "esphome/components/http_request/httplib.h",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								tests/integration/fixtures/entity_icon.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								tests/integration/fixtures/entity_icon.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: icon-test
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
 | 
			
		||||
# Test entities with custom icons
 | 
			
		||||
sensor:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Sensor With Icon"
 | 
			
		||||
    icon: "mdi:temperature-celsius"
 | 
			
		||||
    unit_of_measurement: "°C"
 | 
			
		||||
    update_interval: 1s
 | 
			
		||||
    lambda: |-
 | 
			
		||||
      return 25.5;
 | 
			
		||||
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Sensor Without Icon"
 | 
			
		||||
    unit_of_measurement: "%"
 | 
			
		||||
    update_interval: 1s
 | 
			
		||||
    lambda: |-
 | 
			
		||||
      return 50.0;
 | 
			
		||||
 | 
			
		||||
binary_sensor:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Binary Sensor With Icon"
 | 
			
		||||
    icon: "mdi:motion-sensor"
 | 
			
		||||
    lambda: |-
 | 
			
		||||
      return true;
 | 
			
		||||
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Binary Sensor Without Icon"
 | 
			
		||||
    lambda: |-
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
text_sensor:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Text Sensor With Icon"
 | 
			
		||||
    icon: "mdi:text-box"
 | 
			
		||||
    lambda: |-
 | 
			
		||||
      return {"Hello Icons"};
 | 
			
		||||
 | 
			
		||||
switch:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Switch With Icon"
 | 
			
		||||
    icon: "mdi:toggle-switch"
 | 
			
		||||
    optimistic: true
 | 
			
		||||
 | 
			
		||||
button:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Button With Icon"
 | 
			
		||||
    icon: "mdi:gesture-tap-button"
 | 
			
		||||
    on_press:
 | 
			
		||||
      - logger.log: "Button with icon pressed"
 | 
			
		||||
 | 
			
		||||
number:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Number With Icon"
 | 
			
		||||
    icon: "mdi:numeric"
 | 
			
		||||
    initial_value: 42
 | 
			
		||||
    min_value: 0
 | 
			
		||||
    max_value: 100
 | 
			
		||||
    step: 1
 | 
			
		||||
    optimistic: true
 | 
			
		||||
 | 
			
		||||
select:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Select With Icon"
 | 
			
		||||
    icon: "mdi:format-list-bulleted"
 | 
			
		||||
    options:
 | 
			
		||||
      - "Option A"
 | 
			
		||||
      - "Option B"
 | 
			
		||||
      - "Option C"
 | 
			
		||||
    initial_option: "Option A"
 | 
			
		||||
    optimistic: true
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_bulk_cleanup_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_(
 | 
			
		||||
    "SchedulerBulkCleanupComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
#include "scheduler_bulk_cleanup_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_bulk_cleanup_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bulk_cleanup";
 | 
			
		||||
 | 
			
		||||
void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
 | 
			
		||||
  ESP_LOGI(TAG, "Starting bulk cleanup test...");
 | 
			
		||||
 | 
			
		||||
  // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10)
 | 
			
		||||
  ESP_LOGI(TAG, "Scheduling 25 timeouts...");
 | 
			
		||||
  for (int i = 0; i < 25; i++) {
 | 
			
		||||
    std::string name = "bulk_timeout_" + std::to_string(i);
 | 
			
		||||
    App.scheduler.set_timeout(this, name, 2500, [i]() {
 | 
			
		||||
      // These should never execute as we'll cancel them
 | 
			
		||||
      ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Cancel all of them to mark for removal
 | 
			
		||||
  ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup...");
 | 
			
		||||
  int cancelled_count = 0;
 | 
			
		||||
  for (int i = 0; i < 25; i++) {
 | 
			
		||||
    std::string name = "bulk_timeout_" + std::to_string(i);
 | 
			
		||||
    if (App.scheduler.cancel_timeout(this, name)) {
 | 
			
		||||
      cancelled_count++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count);
 | 
			
		||||
 | 
			
		||||
  // At this point we have 25 items marked for removal
 | 
			
		||||
  // The next scheduler.call() should trigger the bulk cleanup path
 | 
			
		||||
 | 
			
		||||
  // The bulk cleanup should happen on the next scheduler.call() after cancelling items
 | 
			
		||||
  // Log that we expect bulk cleanup to be triggered
 | 
			
		||||
  ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25);
 | 
			
		||||
  ESP_LOGI(TAG, "Items before cleanup: 25+, after: <unknown>");
 | 
			
		||||
 | 
			
		||||
  // Schedule an interval that will execute multiple times to verify scheduler still works
 | 
			
		||||
  static int cleanup_check_count = 0;
 | 
			
		||||
  App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() {
 | 
			
		||||
    cleanup_check_count++;
 | 
			
		||||
    ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count);
 | 
			
		||||
 | 
			
		||||
    if (cleanup_check_count >= 5) {
 | 
			
		||||
      // Cancel the interval
 | 
			
		||||
      App.scheduler.cancel_interval(this, "cleanup_checker");
 | 
			
		||||
      ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup");
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup
 | 
			
		||||
  static int post_cleanup_count = 0;
 | 
			
		||||
  for (int i = 0; i < 5; i++) {
 | 
			
		||||
    std::string name = "post_cleanup_" + std::to_string(i);
 | 
			
		||||
    App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() {
 | 
			
		||||
      ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i);
 | 
			
		||||
      post_cleanup_count++;
 | 
			
		||||
      if (post_cleanup_count >= 5) {
 | 
			
		||||
        ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_bulk_cleanup_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_bulk_cleanup_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerBulkCleanupComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void trigger_bulk_cleanup();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_bulk_cleanup_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_heap_stress_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_heap_stress_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_(
 | 
			
		||||
    "SchedulerHeapStressComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,104 @@
 | 
			
		||||
#include "heap_scheduler_stress_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <random>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_heap_stress_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_heap_stress";
 | 
			
		||||
 | 
			
		||||
void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerHeapStressComponent::run_multi_thread_test() {
 | 
			
		||||
  // Use member variables instead of static to avoid issues
 | 
			
		||||
  this->total_callbacks_ = 0;
 | 
			
		||||
  this->executed_callbacks_ = 0;
 | 
			
		||||
  static constexpr int NUM_THREADS = 10;
 | 
			
		||||
  static constexpr int CALLBACKS_PER_THREAD = 100;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval");
 | 
			
		||||
 | 
			
		||||
  // Ensure we're starting clean
 | 
			
		||||
  ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(),
 | 
			
		||||
           this->executed_callbacks_.load());
 | 
			
		||||
 | 
			
		||||
  // Track start time
 | 
			
		||||
  auto start_time = std::chrono::steady_clock::now();
 | 
			
		||||
 | 
			
		||||
  // Create threads
 | 
			
		||||
  std::vector<std::thread> threads;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD);
 | 
			
		||||
 | 
			
		||||
  threads.reserve(NUM_THREADS);
 | 
			
		||||
  for (int i = 0; i < NUM_THREADS; i++) {
 | 
			
		||||
    threads.emplace_back([this, i]() {
 | 
			
		||||
      ESP_LOGV(TAG, "Thread %d starting", i);
 | 
			
		||||
 | 
			
		||||
      // Random number generator for this thread
 | 
			
		||||
      std::random_device rd;
 | 
			
		||||
      std::mt19937 gen(rd());
 | 
			
		||||
      std::uniform_int_distribution<> timeout_dist(1, 100);    // 1-100ms timeouts
 | 
			
		||||
      std::uniform_int_distribution<> interval_dist(10, 200);  // 10-200ms intervals
 | 
			
		||||
      std::uniform_int_distribution<> type_dist(0, 1);         // 0=timeout, 1=interval
 | 
			
		||||
 | 
			
		||||
      // Each thread directly calls set_timeout/set_interval without any locking
 | 
			
		||||
      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
 | 
			
		||||
        int callback_id = this->total_callbacks_.fetch_add(1);
 | 
			
		||||
        bool use_interval = (type_dist(gen) == 1);
 | 
			
		||||
 | 
			
		||||
        ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id);
 | 
			
		||||
 | 
			
		||||
        // Capture this pointer safely for the lambda
 | 
			
		||||
        auto *component = this;
 | 
			
		||||
 | 
			
		||||
        if (use_interval) {
 | 
			
		||||
          // Use set_interval with random interval time
 | 
			
		||||
          uint32_t interval_ms = interval_dist(gen);
 | 
			
		||||
 | 
			
		||||
          this->set_interval(interval_ms, [component, i, j, callback_id]() {
 | 
			
		||||
            component->executed_callbacks_.fetch_add(1);
 | 
			
		||||
            ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j);
 | 
			
		||||
 | 
			
		||||
            // Cancel the interval after first execution to avoid flooding
 | 
			
		||||
            return false;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Use set_timeout with random timeout
 | 
			
		||||
          uint32_t timeout_ms = timeout_dist(gen);
 | 
			
		||||
 | 
			
		||||
          this->set_timeout(timeout_ms, [component, i, j, callback_id]() {
 | 
			
		||||
            component->executed_callbacks_.fetch_add(1);
 | 
			
		||||
            ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j);
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Small random delay to increase contention
 | 
			
		||||
        if (j % 10 == 0) {
 | 
			
		||||
          std::this_thread::sleep_for(std::chrono::microseconds(100));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGV(TAG, "Thread %d finished", i);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for all threads to complete
 | 
			
		||||
  for (auto &t : threads) {
 | 
			
		||||
    t.join();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto end_time = std::chrono::steady_clock::now();
 | 
			
		||||
  auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
 | 
			
		||||
  ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_heap_stress_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_heap_stress_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerHeapStressComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_multi_thread_test();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  std::atomic<int> total_callbacks_{0};
 | 
			
		||||
  std::atomic<int> executed_callbacks_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_heap_stress_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_rapid_cancellation_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_(
 | 
			
		||||
    "SchedulerRapidCancellationComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
#include "rapid_cancellation_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <random>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_rapid_cancellation_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_rapid_cancellation";
 | 
			
		||||
 | 
			
		||||
void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() {
 | 
			
		||||
  ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names");
 | 
			
		||||
 | 
			
		||||
  // Reset counters
 | 
			
		||||
  this->total_scheduled_ = 0;
 | 
			
		||||
  this->total_executed_ = 0;
 | 
			
		||||
 | 
			
		||||
  static constexpr int NUM_THREADS = 4;              // Number of threads to create
 | 
			
		||||
  static constexpr int NUM_NAMES = 10;               // Only 10 unique names
 | 
			
		||||
  static constexpr int OPERATIONS_PER_THREAD = 100;  // Each thread does 100 operations
 | 
			
		||||
 | 
			
		||||
  // Create threads that will all fight over the same timeout names
 | 
			
		||||
  std::vector<std::thread> threads;
 | 
			
		||||
  threads.reserve(NUM_THREADS);
 | 
			
		||||
 | 
			
		||||
  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
 | 
			
		||||
    threads.emplace_back([this]() {
 | 
			
		||||
      for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
 | 
			
		||||
        // Use modulo to ensure multiple threads use the same names
 | 
			
		||||
        int name_index = i % NUM_NAMES;
 | 
			
		||||
        std::stringstream ss;
 | 
			
		||||
        ss << "shared_timeout_" << name_index;
 | 
			
		||||
        std::string name = ss.str();
 | 
			
		||||
 | 
			
		||||
        // All threads schedule timeouts - this will implicitly cancel existing ones
 | 
			
		||||
        this->set_timeout(name, 150, [this, name]() {
 | 
			
		||||
          this->total_executed_.fetch_add(1);
 | 
			
		||||
          ESP_LOGI(TAG, "Executed callback '%s'", name.c_str());
 | 
			
		||||
        });
 | 
			
		||||
        this->total_scheduled_.fetch_add(1);
 | 
			
		||||
 | 
			
		||||
        // Small delay to increase chance of race conditions
 | 
			
		||||
        if (i % 10 == 0) {
 | 
			
		||||
          std::this_thread::sleep_for(std::chrono::microseconds(100));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for all threads to complete
 | 
			
		||||
  for (auto &t : threads) {
 | 
			
		||||
    t.join();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load());
 | 
			
		||||
 | 
			
		||||
  // Give some time for any remaining callbacks to execute
 | 
			
		||||
  this->set_timeout("final_timeout", 200, [this]() {
 | 
			
		||||
    ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:");
 | 
			
		||||
    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
 | 
			
		||||
    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
 | 
			
		||||
 | 
			
		||||
    // Calculate implicit cancellations (timeouts replaced when scheduling same name)
 | 
			
		||||
    int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load();
 | 
			
		||||
    ESP_LOGI(TAG, "  Implicit cancellations (replaced): %d", implicit_cancellations);
 | 
			
		||||
    ESP_LOGI(TAG, "  Total accounted: %d (executed + implicit cancellations)",
 | 
			
		||||
             this->total_executed_.load() + implicit_cancellations);
 | 
			
		||||
 | 
			
		||||
    // Final message to signal test completion - ensures all stats are logged before test ends
 | 
			
		||||
    ESP_LOGI(TAG, "Test finished - all statistics reported");
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_rapid_cancellation_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_rapid_cancellation_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerRapidCancellationComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_rapid_cancellation_test();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  std::atomic<int> total_scheduled_{0};
 | 
			
		||||
  std::atomic<int> total_executed_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_rapid_cancellation_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_recursive_timeout_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_(
 | 
			
		||||
    "SchedulerRecursiveTimeoutComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
#include "recursive_timeout_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_recursive_timeout_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_recursive_timeout";
 | 
			
		||||
 | 
			
		||||
void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() {
 | 
			
		||||
  ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout");
 | 
			
		||||
 | 
			
		||||
  // Reset state
 | 
			
		||||
  this->nested_level_ = 0;
 | 
			
		||||
 | 
			
		||||
  // Schedule the initial timeout with 1ms delay
 | 
			
		||||
  this->set_timeout(1, [this]() {
 | 
			
		||||
    ESP_LOGI(TAG, "Executing initial timeout");
 | 
			
		||||
    this->nested_level_ = 1;
 | 
			
		||||
 | 
			
		||||
    // From within this timeout, schedule another timeout with 1ms delay
 | 
			
		||||
    this->set_timeout(1, [this]() {
 | 
			
		||||
      ESP_LOGI(TAG, "Executing nested timeout 1");
 | 
			
		||||
      this->nested_level_ = 2;
 | 
			
		||||
 | 
			
		||||
      // From within this nested timeout, schedule yet another timeout with 1ms delay
 | 
			
		||||
      this->set_timeout(1, [this]() {
 | 
			
		||||
        ESP_LOGI(TAG, "Executing nested timeout 2");
 | 
			
		||||
        this->nested_level_ = 3;
 | 
			
		||||
 | 
			
		||||
        // Test complete
 | 
			
		||||
        ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_recursive_timeout_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_recursive_timeout_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerRecursiveTimeoutComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_recursive_timeout_test();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  int nested_level_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_recursive_timeout_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_simultaneous_callbacks_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerSimultaneousCallbacksComponent = (
 | 
			
		||||
    scheduler_simultaneous_callbacks_component_ns.class_(
 | 
			
		||||
        "SchedulerSimultaneousCallbacksComponent", cg.Component
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
#include "simultaneous_callbacks_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_simultaneous_callbacks_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_simultaneous_callbacks";
 | 
			
		||||
 | 
			
		||||
void SchedulerSimultaneousCallbacksComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() {
 | 
			
		||||
  ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now");
 | 
			
		||||
 | 
			
		||||
  // Reset counters
 | 
			
		||||
  this->total_scheduled_ = 0;
 | 
			
		||||
  this->total_executed_ = 0;
 | 
			
		||||
  this->callbacks_at_once_ = 0;
 | 
			
		||||
  this->max_concurrent_ = 0;
 | 
			
		||||
 | 
			
		||||
  static constexpr int NUM_THREADS = 10;
 | 
			
		||||
  static constexpr int CALLBACKS_PER_THREAD = 100;
 | 
			
		||||
  static constexpr uint32_t DELAY_MS = 1;  // All callbacks scheduled for 1ms from now
 | 
			
		||||
 | 
			
		||||
  // Create threads for concurrent scheduling
 | 
			
		||||
  std::vector<std::thread> threads;
 | 
			
		||||
  threads.reserve(NUM_THREADS);
 | 
			
		||||
 | 
			
		||||
  // Record start time for synchronization
 | 
			
		||||
  auto start_time = std::chrono::steady_clock::now();
 | 
			
		||||
 | 
			
		||||
  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
 | 
			
		||||
    threads.emplace_back([this, thread_id, start_time]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id);
 | 
			
		||||
 | 
			
		||||
      // Wait a tiny bit to ensure all threads start roughly together
 | 
			
		||||
      std::this_thread::sleep_until(start_time + std::chrono::microseconds(100));
 | 
			
		||||
 | 
			
		||||
      for (int i = 0; i < CALLBACKS_PER_THREAD; i++) {
 | 
			
		||||
        // Create unique name for each callback
 | 
			
		||||
        std::stringstream ss;
 | 
			
		||||
        ss << "thread_" << thread_id << "_cb_" << i;
 | 
			
		||||
        std::string name = ss.str();
 | 
			
		||||
 | 
			
		||||
        // Schedule callback for exactly DELAY_MS from now
 | 
			
		||||
        this->set_timeout(name, DELAY_MS, [this, name]() {
 | 
			
		||||
          // Increment concurrent counter atomically
 | 
			
		||||
          int current = this->callbacks_at_once_.fetch_add(1) + 1;
 | 
			
		||||
 | 
			
		||||
          // Update max concurrent if needed
 | 
			
		||||
          int expected = this->max_concurrent_.load();
 | 
			
		||||
          while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) {
 | 
			
		||||
            // Loop until we successfully update or someone else set a higher value
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current);
 | 
			
		||||
 | 
			
		||||
          // Simulate some minimal work
 | 
			
		||||
          std::atomic<int> work{0};
 | 
			
		||||
          for (int j = 0; j < 10; j++) {
 | 
			
		||||
            work.fetch_add(j);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Increment executed counter
 | 
			
		||||
          this->total_executed_.fetch_add(1);
 | 
			
		||||
 | 
			
		||||
          // Decrement concurrent counter
 | 
			
		||||
          this->callbacks_at_once_.fetch_sub(1);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this->total_scheduled_.fetch_add(1);
 | 
			
		||||
        ESP_LOGV(TAG, "Scheduled callback %s", name.c_str());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for all threads to complete scheduling
 | 
			
		||||
  for (auto &t : threads) {
 | 
			
		||||
    t.join();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load());
 | 
			
		||||
 | 
			
		||||
  // Schedule a final timeout to check results after all callbacks should have executed
 | 
			
		||||
  this->set_timeout("final_check", 100, [this]() {
 | 
			
		||||
    ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load());
 | 
			
		||||
    ESP_LOGI(TAG, "Statistics:");
 | 
			
		||||
    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
 | 
			
		||||
    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
 | 
			
		||||
    ESP_LOGI(TAG, "  Max concurrent callbacks: %d", this->max_concurrent_.load());
 | 
			
		||||
 | 
			
		||||
    if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) {
 | 
			
		||||
      ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD,
 | 
			
		||||
               this->total_executed_.load());
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_simultaneous_callbacks_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_simultaneous_callbacks_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerSimultaneousCallbacksComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_simultaneous_callbacks_test();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  std::atomic<int> total_scheduled_{0};
 | 
			
		||||
  std::atomic<int> total_executed_{0};
 | 
			
		||||
  std::atomic<int> callbacks_at_once_{0};
 | 
			
		||||
  std::atomic<int> max_concurrent_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_simultaneous_callbacks_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_string_lifetime_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_(
 | 
			
		||||
    "SchedulerStringLifetimeComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,275 @@
 | 
			
		||||
#include "string_lifetime_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_string_lifetime_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_string_lifetime";
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
 | 
			
		||||
  ESP_LOGI(TAG, "Starting string lifetime tests");
 | 
			
		||||
 | 
			
		||||
  this->tests_passed_ = 0;
 | 
			
		||||
  this->tests_failed_ = 0;
 | 
			
		||||
 | 
			
		||||
  // Run each test
 | 
			
		||||
  test_temporary_string_lifetime();
 | 
			
		||||
  test_scope_exit_string();
 | 
			
		||||
  test_vector_reallocation();
 | 
			
		||||
  test_string_move_semantics();
 | 
			
		||||
  test_lambda_capture_lifetime();
 | 
			
		||||
 | 
			
		||||
  // Schedule final check
 | 
			
		||||
  this->set_timeout("final_check", 200, [this]() {
 | 
			
		||||
    ESP_LOGI(TAG, "String lifetime tests complete");
 | 
			
		||||
    ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
 | 
			
		||||
    ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
 | 
			
		||||
 | 
			
		||||
    if (this->tests_failed_ == 0) {
 | 
			
		||||
      ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_test1() {
 | 
			
		||||
  test_temporary_string_lifetime();
 | 
			
		||||
  // Wait for all callbacks to execute
 | 
			
		||||
  this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_test2() {
 | 
			
		||||
  test_scope_exit_string();
 | 
			
		||||
  // Wait for all callbacks to execute
 | 
			
		||||
  this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_test3() {
 | 
			
		||||
  test_vector_reallocation();
 | 
			
		||||
  // Wait for all callbacks to execute
 | 
			
		||||
  this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_test4() {
 | 
			
		||||
  test_string_move_semantics();
 | 
			
		||||
  // Wait for all callbacks to execute
 | 
			
		||||
  this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_test5() {
 | 
			
		||||
  test_lambda_capture_lifetime();
 | 
			
		||||
  // Wait for all callbacks to execute
 | 
			
		||||
  this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::run_final_check() {
 | 
			
		||||
  ESP_LOGI(TAG, "String lifetime tests complete");
 | 
			
		||||
  ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
 | 
			
		||||
  ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
 | 
			
		||||
 | 
			
		||||
  if (this->tests_failed_ == 0) {
 | 
			
		||||
    ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
 | 
			
		||||
  ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names");
 | 
			
		||||
 | 
			
		||||
  // Test with a temporary string that goes out of scope immediately
 | 
			
		||||
  {
 | 
			
		||||
    std::string temp_name = "temp_callback_" + std::to_string(12345);
 | 
			
		||||
 | 
			
		||||
    // Schedule with temporary string name - scheduler must copy/store this
 | 
			
		||||
    this->set_timeout(temp_name, 1, [this]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Callback for temp string name executed");
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // String goes out of scope here, but scheduler should have made a copy
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Test with rvalue string as name
 | 
			
		||||
  this->set_timeout(std::string("rvalue_test"), 2, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Rvalue string name callback executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Test cancelling with reconstructed string
 | 
			
		||||
  {
 | 
			
		||||
    std::string cancel_name = "cancel_test_" + std::to_string(999);
 | 
			
		||||
    this->set_timeout(cancel_name, 100, [this]() {
 | 
			
		||||
      ESP_LOGE(TAG, "This should have been cancelled!");
 | 
			
		||||
      this->tests_failed_++;
 | 
			
		||||
    });
 | 
			
		||||
  }  // cancel_name goes out of scope
 | 
			
		||||
 | 
			
		||||
  // Reconstruct the same string to cancel
 | 
			
		||||
  std::string cancel_name_2 = "cancel_test_" + std::to_string(999);
 | 
			
		||||
  bool cancelled = this->cancel_timeout(cancel_name_2);
 | 
			
		||||
  if (cancelled) {
 | 
			
		||||
    ESP_LOGD(TAG, "Successfully cancelled with reconstructed string");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to cancel with reconstructed string");
 | 
			
		||||
    this->tests_failed_++;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::test_scope_exit_string() {
 | 
			
		||||
  ESP_LOGI(TAG, "Test 2: Scope exit string names");
 | 
			
		||||
 | 
			
		||||
  // Create string names in a limited scope
 | 
			
		||||
  {
 | 
			
		||||
    std::string scoped_name = "scoped_timeout_" + std::to_string(555);
 | 
			
		||||
 | 
			
		||||
    // Schedule with scoped string name
 | 
			
		||||
    this->set_timeout(scoped_name, 3, [this]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Scoped name callback executed");
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // scoped_name goes out of scope here
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Test with dynamically allocated string name
 | 
			
		||||
  {
 | 
			
		||||
    auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777));
 | 
			
		||||
 | 
			
		||||
    this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Dynamic string name callback executed");
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
      delete dynamic_name;  // Clean up in callback
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Pointer goes out of scope but string object remains until callback
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Test multiple timeouts with same dynamically created name
 | 
			
		||||
  for (int i = 0; i < 3; i++) {
 | 
			
		||||
    std::string loop_name = "loop_timeout_" + std::to_string(i);
 | 
			
		||||
    this->set_timeout(loop_name, 5 + i * 1, [this, i]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Loop timeout %d executed", i);
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
    // loop_name destroyed and recreated each iteration
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::test_vector_reallocation() {
 | 
			
		||||
  ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names");
 | 
			
		||||
 | 
			
		||||
  // Create a vector that will reallocate
 | 
			
		||||
  std::vector<std::string> names;
 | 
			
		||||
  names.reserve(2);  // Small initial capacity to force reallocation
 | 
			
		||||
 | 
			
		||||
  // Schedule callbacks with string names from vector
 | 
			
		||||
  for (int i = 0; i < 10; i++) {
 | 
			
		||||
    names.push_back("vector_cb_" + std::to_string(i));
 | 
			
		||||
    // Use the string from vector as timeout name
 | 
			
		||||
    this->set_timeout(names.back(), 8 + i * 1, [this, i]() {
 | 
			
		||||
      ESP_LOGV(TAG, "Vector name callback %d executed", i);
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Force reallocation by adding more elements
 | 
			
		||||
  // This will move all strings to new memory locations
 | 
			
		||||
  for (int i = 10; i < 50; i++) {
 | 
			
		||||
    names.push_back("realloc_trigger_" + std::to_string(i));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add more timeouts after reallocation to ensure old names still work
 | 
			
		||||
  for (int i = 50; i < 55; i++) {
 | 
			
		||||
    names.push_back("post_realloc_" + std::to_string(i));
 | 
			
		||||
    this->set_timeout(names.back(), 20 + (i - 50), [this]() {
 | 
			
		||||
      ESP_LOGV(TAG, "Post-reallocation callback executed");
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Clear the vector while timeouts are still pending
 | 
			
		||||
  names.clear();
 | 
			
		||||
  ESP_LOGD(TAG, "Vector cleared - all string names destroyed");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::test_string_move_semantics() {
 | 
			
		||||
  ESP_LOGI(TAG, "Test 4: String move semantics for timeout names");
 | 
			
		||||
 | 
			
		||||
  // Test moving string names
 | 
			
		||||
  std::string original = "move_test_original";
 | 
			
		||||
  std::string moved = std::move(original);
 | 
			
		||||
 | 
			
		||||
  // Schedule with moved string as name
 | 
			
		||||
  this->set_timeout(moved, 30, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Moved string name callback executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // original is now empty, try to use it as a different timeout name
 | 
			
		||||
  original = "reused_after_move";
 | 
			
		||||
  this->set_timeout(original, 32, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Reused string name callback executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() {
 | 
			
		||||
  ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios");
 | 
			
		||||
 | 
			
		||||
  // Test scheduling with name built in lambda
 | 
			
		||||
  [this]() {
 | 
			
		||||
    std::string lambda_name = "lambda_built_name_" + std::to_string(888);
 | 
			
		||||
    this->set_timeout(lambda_name, 38, [this]() {
 | 
			
		||||
      ESP_LOGD(TAG, "Lambda-built name callback executed");
 | 
			
		||||
      this->tests_passed_++;
 | 
			
		||||
    });
 | 
			
		||||
  }();  // Lambda executes and lambda_name is destroyed
 | 
			
		||||
 | 
			
		||||
  // Test with shared_ptr name
 | 
			
		||||
  auto shared_name = std::make_shared<std::string>("shared_ptr_timeout");
 | 
			
		||||
  this->set_timeout(*shared_name, 40, [this, shared_name]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Shared_ptr name callback executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
  shared_name.reset();  // Release the shared_ptr
 | 
			
		||||
 | 
			
		||||
  // Test overwriting timeout with same name
 | 
			
		||||
  std::string overwrite_name = "overwrite_test";
 | 
			
		||||
  this->set_timeout(overwrite_name, 1000, [this]() {
 | 
			
		||||
    ESP_LOGE(TAG, "This should have been overwritten!");
 | 
			
		||||
    this->tests_failed_++;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Overwrite with shorter timeout
 | 
			
		||||
  this->set_timeout(overwrite_name, 42, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Overwritten timeout executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Test very long string name
 | 
			
		||||
  std::string long_name;
 | 
			
		||||
  for (int i = 0; i < 100; i++) {
 | 
			
		||||
    long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_";
 | 
			
		||||
  }
 | 
			
		||||
  this->set_timeout(long_name, 44, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Very long name timeout executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Test empty string as name
 | 
			
		||||
  this->set_timeout("", 46, [this]() {
 | 
			
		||||
    ESP_LOGD(TAG, "Empty string name timeout executed");
 | 
			
		||||
    this->tests_passed_++;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_string_lifetime_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_string_lifetime_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerStringLifetimeComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_string_lifetime_test();
 | 
			
		||||
 | 
			
		||||
  // Individual test methods exposed as services
 | 
			
		||||
  void run_test1();
 | 
			
		||||
  void run_test2();
 | 
			
		||||
  void run_test3();
 | 
			
		||||
  void run_test4();
 | 
			
		||||
  void run_test5();
 | 
			
		||||
  void run_final_check();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void test_temporary_string_lifetime();
 | 
			
		||||
  void test_scope_exit_string();
 | 
			
		||||
  void test_vector_reallocation();
 | 
			
		||||
  void test_string_move_semantics();
 | 
			
		||||
  void test_lambda_capture_lifetime();
 | 
			
		||||
 | 
			
		||||
  int tests_passed_{0};
 | 
			
		||||
  int tests_failed_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_string_lifetime_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace(
 | 
			
		||||
    "scheduler_string_name_stress_component"
 | 
			
		||||
)
 | 
			
		||||
SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_(
 | 
			
		||||
    "SchedulerStringNameStressComponent", cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -0,0 +1,110 @@
 | 
			
		||||
#include "string_name_stress_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_string_name_stress_component {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "scheduler_string_name_stress";
 | 
			
		||||
 | 
			
		||||
void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); }
 | 
			
		||||
 | 
			
		||||
void SchedulerStringNameStressComponent::run_string_name_stress_test() {
 | 
			
		||||
  // Use member variables to reset state
 | 
			
		||||
  this->total_callbacks_ = 0;
 | 
			
		||||
  this->executed_callbacks_ = 0;
 | 
			
		||||
  static constexpr int NUM_THREADS = 10;
 | 
			
		||||
  static constexpr int CALLBACKS_PER_THREAD = 100;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names");
 | 
			
		||||
  ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management");
 | 
			
		||||
 | 
			
		||||
  // Track start time
 | 
			
		||||
  auto start_time = std::chrono::steady_clock::now();
 | 
			
		||||
 | 
			
		||||
  // Create threads
 | 
			
		||||
  std::vector<std::thread> threads;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS,
 | 
			
		||||
           CALLBACKS_PER_THREAD);
 | 
			
		||||
 | 
			
		||||
  threads.reserve(NUM_THREADS);
 | 
			
		||||
  for (int i = 0; i < NUM_THREADS; i++) {
 | 
			
		||||
    threads.emplace_back([this, i]() {
 | 
			
		||||
      ESP_LOGV(TAG, "Thread %d starting", i);
 | 
			
		||||
 | 
			
		||||
      // Each thread schedules callbacks with dynamically created string names
 | 
			
		||||
      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
 | 
			
		||||
        int callback_id = this->total_callbacks_.fetch_add(1);
 | 
			
		||||
 | 
			
		||||
        // Create a dynamic string name - this will test memory management
 | 
			
		||||
        std::stringstream ss;
 | 
			
		||||
        ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id;
 | 
			
		||||
        std::string dynamic_name = ss.str();
 | 
			
		||||
 | 
			
		||||
        ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str());
 | 
			
		||||
 | 
			
		||||
        // Capture necessary values for the lambda
 | 
			
		||||
        auto *component = this;
 | 
			
		||||
 | 
			
		||||
        // Schedule with std::string name - this tests the string overload
 | 
			
		||||
        // Use varying delays to stress the heap scheduler
 | 
			
		||||
        uint32_t delay = 1 + (callback_id % 50);
 | 
			
		||||
 | 
			
		||||
        // Also test nested scheduling from callbacks
 | 
			
		||||
        if (j % 10 == 0) {
 | 
			
		||||
          // Every 10th callback schedules another callback
 | 
			
		||||
          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
 | 
			
		||||
            component->executed_callbacks_.fetch_add(1);
 | 
			
		||||
            ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id);
 | 
			
		||||
 | 
			
		||||
            // Schedule another timeout from within this callback with a new dynamic name
 | 
			
		||||
            std::string nested_name = "nested_from_" + std::to_string(callback_id);
 | 
			
		||||
            component->set_timeout(nested_name, 1, [callback_id]() {
 | 
			
		||||
              ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id);
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          // Regular callback
 | 
			
		||||
          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
 | 
			
		||||
            component->executed_callbacks_.fetch_add(1);
 | 
			
		||||
            ESP_LOGV(TAG, "Executed string-named callback %d", callback_id);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add some timing variations to increase race conditions
 | 
			
		||||
        if (j % 5 == 0) {
 | 
			
		||||
          std::this_thread::sleep_for(std::chrono::microseconds(100));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGV(TAG, "Thread %d finished scheduling", i);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for all threads to complete scheduling
 | 
			
		||||
  for (auto &t : threads) {
 | 
			
		||||
    t.join();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto end_time = std::chrono::steady_clock::now();
 | 
			
		||||
  auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
 | 
			
		||||
  ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time,
 | 
			
		||||
           this->total_callbacks_.load());
 | 
			
		||||
 | 
			
		||||
  // Give some time for callbacks to execute
 | 
			
		||||
  ESP_LOGI(TAG, "Waiting for callbacks to execute...");
 | 
			
		||||
 | 
			
		||||
  // Schedule a final callback to signal completion
 | 
			
		||||
  this->set_timeout("test_complete", 2000, [this]() {
 | 
			
		||||
    ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(),
 | 
			
		||||
             this->total_callbacks_.load());
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_string_name_stress_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace scheduler_string_name_stress_component {
 | 
			
		||||
 | 
			
		||||
class SchedulerStringNameStressComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void run_string_name_stress_test();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  std::atomic<int> total_callbacks_{0};
 | 
			
		||||
  std::atomic<int> executed_callbacks_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace scheduler_string_name_stress_component
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										80
									
								
								tests/integration/fixtures/light_calls.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								tests/integration/fixtures/light_calls.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: light-calls-test
 | 
			
		||||
host:
 | 
			
		||||
api:  # Port will be automatically injected
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
# Test outputs for RGBCW light
 | 
			
		||||
output:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_red
 | 
			
		||||
    type: float
 | 
			
		||||
    write_action:
 | 
			
		||||
      - logger.log:
 | 
			
		||||
          format: "Red output: %.2f"
 | 
			
		||||
          args: [state]
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_green
 | 
			
		||||
    type: float
 | 
			
		||||
    write_action:
 | 
			
		||||
      - logger.log:
 | 
			
		||||
          format: "Green output: %.2f"
 | 
			
		||||
          args: [state]
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_blue
 | 
			
		||||
    type: float
 | 
			
		||||
    write_action:
 | 
			
		||||
      - logger.log:
 | 
			
		||||
          format: "Blue output: %.2f"
 | 
			
		||||
          args: [state]
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_cold_white
 | 
			
		||||
    type: float
 | 
			
		||||
    write_action:
 | 
			
		||||
      - logger.log:
 | 
			
		||||
          format: "Cold white output: %.2f"
 | 
			
		||||
          args: [state]
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_warm_white
 | 
			
		||||
    type: float
 | 
			
		||||
    write_action:
 | 
			
		||||
      - logger.log:
 | 
			
		||||
          format: "Warm white output: %.2f"
 | 
			
		||||
          args: [state]
 | 
			
		||||
 | 
			
		||||
light:
 | 
			
		||||
  - platform: rgbww
 | 
			
		||||
    name: "Test RGBCW Light"
 | 
			
		||||
    id: test_light
 | 
			
		||||
    red: test_red
 | 
			
		||||
    green: test_green
 | 
			
		||||
    blue: test_blue
 | 
			
		||||
    cold_white: test_cold_white
 | 
			
		||||
    warm_white: test_warm_white
 | 
			
		||||
    cold_white_color_temperature: 6536 K
 | 
			
		||||
    warm_white_color_temperature: 2000 K
 | 
			
		||||
    constant_brightness: true
 | 
			
		||||
    effects:
 | 
			
		||||
      - random:
 | 
			
		||||
          name: "Random Effect"
 | 
			
		||||
          transition_length: 100ms
 | 
			
		||||
          update_interval: 200ms
 | 
			
		||||
      - strobe:
 | 
			
		||||
          name: "Strobe Effect"
 | 
			
		||||
      - pulse:
 | 
			
		||||
          name: "Pulse Effect"
 | 
			
		||||
          transition_length: 100ms
 | 
			
		||||
 | 
			
		||||
  # Additional lights to test memory with multiple instances
 | 
			
		||||
  - platform: rgb
 | 
			
		||||
    name: "Test RGB Light"
 | 
			
		||||
    id: test_rgb_light
 | 
			
		||||
    red: test_red
 | 
			
		||||
    green: test_green
 | 
			
		||||
    blue: test_blue
 | 
			
		||||
 | 
			
		||||
  - platform: binary
 | 
			
		||||
    name: "Test Binary Light"
 | 
			
		||||
    id: test_binary_light
 | 
			
		||||
    output: test_red
 | 
			
		||||
							
								
								
									
										23
									
								
								tests/integration/fixtures/scheduler_bulk_cleanup.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tests/integration/fixtures/scheduler_bulk_cleanup.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: scheduler-bulk-cleanup
 | 
			
		||||
 | 
			
		||||
external_components:
 | 
			
		||||
  - source:
 | 
			
		||||
      type: local
 | 
			
		||||
      path: EXTERNAL_COMPONENT_PATH
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  services:
 | 
			
		||||
    - service: trigger_bulk_cleanup
 | 
			
		||||
      then:
 | 
			
		||||
        - lambda: |-
 | 
			
		||||
            auto component = id(bulk_cleanup_component);
 | 
			
		||||
            component->trigger_bulk_cleanup();
 | 
			
		||||
 | 
			
		||||
scheduler_bulk_cleanup_component:
 | 
			
		||||
  id: bulk_cleanup_component
 | 
			
		||||
							
								
								
									
										51
									
								
								tests/integration/fixtures/scheduler_defer_cancel.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/integration/fixtures/scheduler_defer_cancel.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: scheduler-defer-cancel
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  services:
 | 
			
		||||
    - service: test_defer_cancel
 | 
			
		||||
      then:
 | 
			
		||||
        - lambda: |-
 | 
			
		||||
            // Schedule 10 defers with the same name
 | 
			
		||||
            // Only the last one should execute
 | 
			
		||||
            for (int i = 1; i <= 10; i++) {
 | 
			
		||||
              App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() {
 | 
			
		||||
                ESP_LOGI("TEST", "Defer executed: %d", i);
 | 
			
		||||
                // Fire event with the defer number
 | 
			
		||||
                std::string event_type = "defer_executed_" + std::to_string(i);
 | 
			
		||||
                id(test_result)->trigger(event_type);
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Schedule completion notification after all defers
 | 
			
		||||
            App.scheduler.set_timeout(nullptr, "completion", 0, []() {
 | 
			
		||||
              ESP_LOGI("TEST", "Test complete");
 | 
			
		||||
              id(test_complete)->trigger("test_finished");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
event:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_result
 | 
			
		||||
    name: "Test Result"
 | 
			
		||||
    event_types:
 | 
			
		||||
      - "defer_executed_1"
 | 
			
		||||
      - "defer_executed_2"
 | 
			
		||||
      - "defer_executed_3"
 | 
			
		||||
      - "defer_executed_4"
 | 
			
		||||
      - "defer_executed_5"
 | 
			
		||||
      - "defer_executed_6"
 | 
			
		||||
      - "defer_executed_7"
 | 
			
		||||
      - "defer_executed_8"
 | 
			
		||||
      - "defer_executed_9"
 | 
			
		||||
      - "defer_executed_10"
 | 
			
		||||
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_complete
 | 
			
		||||
    name: "Test Complete"
 | 
			
		||||
    event_types:
 | 
			
		||||
      - "test_finished"
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user