mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-01 07:31:51 +00:00 
			
		
		
		
	Compare commits
	
		
			1419 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 573088aadb | ||
|  | 031b1c8bd0 | ||
|  | f95b2ba898 | ||
|  | ea4b573f9a | ||
|  | 8fcbd57f2f | ||
|  | f131186e6b | ||
|  | 20c7778524 | ||
|  | 2d8e86324b | ||
|  | 8ea4d8402f | ||
|  | 2c53408cfc | ||
|  | 33dce6e522 | ||
|  | e213932b7c | ||
|  | 42fb0e2809 | ||
|  | c4de9e87e4 | ||
|  | 918924d697 | ||
|  | e2c16b4baa | ||
|  | 10a9162f48 | ||
|  | fbc884772c | ||
|  | 54e3153f27 | ||
|  | c2e0a01106 | ||
|  | d2c2439b97 | ||
|  | a8d33dd26a | ||
|  | da41a9204e | ||
|  | 5c6368b6b8 | ||
|  | 9bd7060f6b | ||
|  | fb1d178abc | ||
|  | 90c96a0a0f | ||
|  | 1bdf0fdc57 | ||
|  | 4d95ff2ae0 | ||
|  | f36d400058 | ||
|  | c63cf9d151 | ||
|  | 0a02c1461e | ||
|  | b3a69c6c05 | ||
|  | dd113f2972 | ||
|  | 3c5a0091ee | ||
|  | bf65b73569 | ||
|  | a2b123a29a | ||
|  | 3575f52cdf | ||
|  | 52269305ec | ||
|  | 37fabd7c0a | ||
|  | 4aa7ad1e33 | ||
|  | 42e432754e | ||
|  | 2379f02008 | ||
|  | d3145dd95b | ||
|  | ab77dd691b | ||
|  | b54c0fd60a | ||
|  | 75d1eeeffe | ||
|  | 10cea51739 | ||
|  | 83e090cc7e | ||
|  | 583f8f598a | ||
|  | 3e9556c6c2 | ||
|  | 83cba0d7bd | ||
|  | 1d6d0d66dc | ||
|  | 4ed78023b6 | ||
|  | 323209523b | ||
|  | 46a4f4eba9 | ||
|  | 53fda0e96d | ||
|  | 0350eafc1e | ||
|  | 7b8e68c73a | ||
|  | db666e44a7 | ||
|  | 903d033e0f | ||
|  | 19d938ce48 | ||
|  | 653318479a | ||
|  | 2af5fd5210 | ||
|  | 29e388b231 | ||
|  | d9e23fdb5c | ||
|  | 10eacaccba | ||
|  | 23687b2afd | ||
|  | f11ad9ad5b | ||
|  | 74a25a7e76 | ||
|  | 23e04e18f8 | ||
|  | aed5020a83 | ||
|  | 476f1b701b | ||
|  | 7c3a7b68d3 | ||
|  | 75dc0d3fb7 | ||
|  | 9bc4f68d87 | ||
|  | 1029202848 | ||
|  | a831905bba | ||
|  | faffd79545 | ||
|  | 7714147071 | ||
|  | 4da42dedc8 | ||
|  | 28f283d545 | ||
|  | 3048f303c5 | ||
|  | 63a7234767 | ||
|  | c19621e238 | ||
|  | bc96eb9d52 | ||
|  | 7375dde39c | ||
|  | 1b7111affb | ||
|  | a511926aed | ||
|  | 6b36cb95c9 | ||
|  | c13174c318 | ||
|  | d5da341138 | ||
|  | 8fa157581e | ||
|  | 7114d6bdd1 | ||
|  | eca0c21966 | ||
|  | 20c9c410af | ||
|  | 79af437f48 | ||
|  | 6e27003787 | ||
|  | b7b2f3e61c | ||
|  | 9448737a92 | ||
|  | 6f2bf4ec4c | ||
|  | 54cea6c41e | ||
|  | e754d0a58b | ||
|  | 5e44a035a3 | ||
|  | c424fea524 | ||
|  | 6aba1dbd73 | ||
|  | 422fb8f1a5 | ||
|  | 2988bbb8ce | ||
|  | 59299bffc8 | ||
|  | 3410aee42e | ||
|  | 96682f5cbe | ||
|  | bfa3254d6c | ||
|  | 990d1e3bb0 | ||
|  | 755b0bbfc7 | ||
|  | c281351732 | ||
|  | 9f603a474f | ||
|  | bf739506c3 | ||
|  | 3020083564 | ||
|  | 31e90e5544 | ||
|  | 7c9726859f | ||
|  | 7529fb10b4 | ||
|  | ba79e2d7b1 | ||
|  | 7006bd24a5 | ||
|  | 58311c9a0d | ||
|  | ae65f76dfe | ||
|  | 4d380214df | ||
|  | c5ebf7683e | ||
|  | a973adda67 | ||
|  | d9b419eaf5 | ||
|  | 02bf33c548 | ||
|  | b3db04a3d3 | ||
|  | 56034e3e79 | ||
|  | abbd72e802 | ||
|  | 1257640e48 | ||
|  | 2bc9782ce7 | ||
|  | 6583e17810 | ||
|  | 64c8bcef2e | ||
|  | f9da8dbfb8 | ||
|  | 74f7197543 | ||
|  | c21b8bd417 | ||
|  | 1eb658cc5b | ||
|  | 8b251efb75 | ||
|  | 26d25464da | ||
|  | 78b55e22ee | ||
|  | 9ee5227fe0 | ||
|  | e89603fe3b | ||
|  | c0804d665d | ||
|  | a67b85eabf | ||
|  | a47e27885f | ||
|  | 2e66b33672 | ||
|  | e21ef22706 | ||
|  | 93c2878c21 | ||
|  | b3ad6a03e6 | ||
|  | 6e45a7c9af | ||
|  | e17582544e | ||
|  | daa7960031 | ||
|  | 6999cc0581 | ||
|  | 92ad6286aa | ||
|  | 1111aa167f | ||
|  | 143b0d3de4 | ||
|  | 788c41e6f4 | ||
|  | 46b6dcdfbf | ||
|  | d05f641dd0 | ||
|  | 897873496a | ||
|  | b0f6dd7d9c | ||
|  | be5639faf1 | ||
|  | e9a537784e | ||
|  | 35d303809e | ||
|  | 4740f12ce8 | ||
|  | c8e7e275a4 | ||
|  | 077ee5b714 | ||
|  | fa029e8fc7 | ||
|  | ace953bd50 | ||
|  | e190ef9e9b | ||
|  | 2868210d46 | ||
|  | 72f6461871 | ||
|  | 4a95468fd2 | ||
|  | 43319d4c8a | ||
|  | 3b7a7a2262 | ||
|  | de2d21862b | ||
|  | 3d48eb26cd | ||
|  | ab0d38fbda | ||
|  | 2b75e34719 | ||
|  | 0b6c416680 | ||
|  | 7bb2c3c496 | ||
|  | 88cfdc33d4 | ||
|  | a2f1b90238 | ||
|  | 0401ee9428 | ||
|  | 14d7931bd6 | ||
|  | 6b3f3e1da6 | ||
|  | 33f9d66e81 | ||
|  | 46d19d82c2 | ||
|  | c9e7562aff | ||
|  | 8b7aa4c110 | ||
|  | b667ceaced | ||
|  | abdf215d3a | ||
|  | 84836f15db | ||
|  | 8be9f02693 | ||
|  | 1ab1768b6a | ||
|  | 0d13e2040d | ||
|  | fd24b1423c | ||
|  | 66c35a9432 | ||
|  | 45b8810ab8 | ||
|  | ff7d232ee6 | ||
|  | 0cd3af2fcd | ||
|  | 8897a9866d | ||
|  | dc8646cda6 | ||
|  | 353924257a | ||
|  | da3d007d7b | ||
|  | 9e3359cdf2 | ||
|  | 7e626b04f2 | ||
|  | 4eb551864d | ||
|  | e337bd7beb | ||
|  | 57739b8bb0 | ||
|  | 65ca000e6d | ||
|  | bf6874b52e | ||
|  | cecce0f3cb | ||
|  | 4d8f58db94 | ||
|  | 977333a73c | ||
|  | 1215d2ffeb | ||
|  | 9b56f9cc6d | ||
|  | 2e61229aed | ||
|  | 55203143df | ||
|  | 4e4566361f | ||
|  | 4273449003 | ||
|  | f8fae676b1 | ||
|  | 211aee91e5 | ||
|  | 6e3527a88b | ||
|  | 06f9764f51 | ||
|  | 693d813c4b | ||
|  | 61ad2510fc | ||
|  | 53c15f6716 | ||
|  | d4ac2d3c7e | ||
|  | 6f4e8f1fbf | ||
|  | 847cff06b3 | ||
|  | bd34697715 | ||
|  | 6b55df36c7 | ||
|  | b8f9eaecd8 | ||
|  | c8bbc2e84c | ||
|  | 5108b9a8b7 | ||
|  | 8de5af4eec | ||
|  | 6e5e681055 | ||
|  | f6cf99384b | ||
|  | 2b711e532b | ||
|  | 72c6f04a97 | ||
|  | 03e2701bd0 | ||
|  | 051fa3a49f | ||
|  | 7392397630 | ||
|  | 714e2d3e56 | ||
|  | 12d6c1bbca | ||
|  | 7727879f01 | ||
|  | 334e952a34 | ||
|  | f9856135d0 | ||
|  | ba3e5e8ecb | ||
|  | 67ccd0eb7f | ||
|  | 619ce93dec | ||
|  | 9957840dfc | ||
|  | a23ce416ea | ||
|  | 2489f95107 | ||
|  | 7dab1a6082 | ||
|  | f7f8bf4da4 | ||
|  | dd18a219db | ||
|  | dbf4c2c4da | ||
|  | fc847c1de8 | ||
|  | 7fccc9ff86 | ||
|  | dee1d84979 | ||
|  | 65b2d48a6f | ||
|  | 8aeb08f868 | ||
|  | d4857a1727 | ||
|  | 0c032bc431 | ||
|  | 5a103543c4 | ||
|  | 01ab6d3ddc | ||
|  | f2170c633a | ||
|  | c2e52f4b11 | ||
|  | 4843bbd38a | ||
|  | 78ce8f014a | ||
|  | b454f63b36 | ||
|  | db644542ed | ||
|  | 716a8b87e1 | ||
|  | 0f4e274e52 | ||
|  | 576dbd6f0c | ||
|  | c3d00b45f7 | ||
|  | 98b872abc7 | ||
|  | 75026be951 | ||
|  | 47a0ec467a | ||
|  | 9e40d4cf45 | ||
|  | fecae2f740 | ||
|  | 5a01670803 | ||
|  | c2423b18cb | ||
|  | 2363b3dfd6 | ||
|  | 628e47f670 | ||
|  | 7666581c54 | ||
|  | 03c36920ff | ||
|  | abdd6b232f | ||
|  | 07be7ad7e2 | ||
|  | 820e3488d0 | ||
|  | 8c6c45e6c1 | ||
|  | 16bf56b0f9 | ||
|  | 49c01c26f1 | ||
|  | b4a804cc77 | ||
|  | df26ace0f1 | ||
|  | e779a8bcb2 | ||
|  | c458fd18df | ||
|  | 98817a5bbf | ||
|  | c43d8460bd | ||
|  | 17b88f2e3e | ||
|  | dac9768f6a | ||
|  | e8d2ad4ce8 | ||
|  | c3412df169 | ||
|  | fc2b15e307 | ||
|  | bdb1094b47 | ||
|  | 6262fb8fcf | ||
|  | f319472066 | ||
|  | b4a2b50ee0 | ||
|  | 30bb806f26 | ||
|  | 9874d17613 | ||
|  | 13909b7994 | ||
|  | df50e57409 | ||
|  | 3fa67fad32 | ||
|  | 8fbd512952 | ||
|  | 528d3672b4 | ||
|  | fef50afef8 | ||
|  | aa1879082c | ||
|  | d8c943972b | ||
|  | f3ebb4eb39 | ||
|  | f1c0570e3b | ||
|  | aa87c60717 | ||
|  | 92a8ebe1f8 | ||
|  | dd3ffc7f29 | ||
|  | aac3841991 | ||
|  | fb87a1c0bc | ||
|  | 4409471cd1 | ||
|  | 739edce268 | ||
|  | f25f3334d1 | ||
|  | 571935fb3b | ||
|  | 7c39422692 | ||
|  | 731fb1d172 | ||
|  | 40bee2a854 | ||
|  | d69926485c | ||
|  | fe80750743 | ||
|  | 109d737d5d | ||
|  | bd17ee8e33 | ||
|  | f1712cffa8 | ||
|  | 0df6a913b3 | ||
|  | 8a98b69a57 | ||
|  | 4530e4d60f | ||
|  | 4d7c6b28e1 | ||
|  | de603c7565 | ||
|  | a498fb5dcf | ||
|  | 78543e1e15 | ||
|  | 5e72b7196b | ||
|  | a0615a92f0 | ||
|  | dc5b408748 | ||
|  | 387bde665e | ||
|  | 45beea68eb | ||
|  | c457d8835e | ||
|  | 4b51ba3fa4 | ||
|  | 499953e3f4 | ||
|  | 69f1a81e1d | ||
|  | 37fcccbb1c | ||
|  | f3cb179f54 | ||
|  | ba2edbc189 | ||
|  | f33b4a714e | ||
|  | 85d863601b | ||
|  | fe0700166a | ||
|  | d28cf011d1 | ||
|  | 434879ea04 | ||
|  | 7da07303c9 | ||
|  | b33b4481ea | ||
|  | ac631711ab | ||
|  | 265b6ec445 | ||
|  | 61499dbdd8 | ||
|  | 0aaef9293b | ||
|  | 0f0b829bc6 | ||
|  | a9d883b65a | ||
|  | d330e73c1e | ||
|  | 7554e954fe | ||
|  | 752af94a75 | ||
|  | 561d92d402 | ||
|  | 1a69236473 | ||
|  | c86ea99145 | ||
|  | 7661609049 | ||
|  | c38826824f | ||
|  | e890486043 | ||
|  | ccc9fd4a3f | ||
|  | 54fbf5184e | ||
|  | 759df7ae6c | ||
|  | 3d56397e58 | ||
|  | 9f6c64afa6 | ||
|  | 663e18310d | ||
|  | 1a89aa8fbf | ||
|  | e04743e381 | ||
|  | a6957b9d3b | ||
|  | 9816c27031 | ||
|  | ea06740b46 | ||
|  | 9a5ec1b9e6 | ||
|  | 6dcbd1a8ae | ||
|  | 63b0930ae8 | ||
|  | 5382bd2a97 | ||
|  | de1fbd390b | ||
|  | af23357dca | ||
|  | 0fbe6c0d8b | ||
|  | 4e1ff31342 | ||
|  | df4224e779 | ||
|  | 5877c57a35 | ||
|  | 7f2ca800c1 | ||
|  | ce7ff15c8a | ||
|  | 88742e0399 | ||
|  | c187cb547c | ||
|  | 42bc960a36 | ||
|  | ba63d266d8 | ||
|  | 90baba4db7 | ||
|  | 1656ced351 | ||
|  | 1dfd15e607 | ||
|  | 5dcaf1241f | ||
|  | 7aa54b6879 | ||
|  | 444e162c92 | ||
|  | bb27eaaf1e | ||
|  | 517f659da8 | ||
|  | 5a92e24662 | ||
|  | 437b236a4d | ||
|  | 14eac3dbce | ||
|  | 132a096ae7 | ||
|  | 440080a753 | ||
|  | f15e3cfb9b | ||
|  | 9d000e9abf | ||
|  | 97fd7493b5 | ||
|  | 4c87658503 | ||
|  | c80e035bd5 | ||
|  | c8ec0bb7ea | ||
|  | 86ae1c5931 | ||
|  | d0958f7cf2 | ||
|  | 982ce1db72 | ||
|  | f042c6e643 | ||
|  | 5fcd26bfe9 | ||
|  | 5717d557f5 | ||
|  | 3bac45e737 | ||
|  | e623989878 | ||
|  | 39cbc6b183 | ||
|  | 749a5e3348 | ||
|  | b0e3ac01e8 | ||
|  | 58123845ff | ||
|  | bfd75d736c | ||
|  | 4e3195b474 | ||
|  | d3a71a1d45 | ||
|  | 555bdac604 | ||
|  | acc8d24a32 | ||
|  | f3cc1e541a | ||
|  | ece72c6b18 | ||
|  | 4e839d42d0 | ||
|  | d429aa8bb8 | ||
|  | 472402745d | ||
|  | 016fac2496 | ||
|  | 79478cdb8a | ||
|  | dbed74b50d | ||
|  | d00ec7e544 | ||
|  | a37ff2dbd9 | ||
|  | 00ddb0a427 | ||
|  | c95887a14a | ||
|  | dc5942a59b | ||
|  | 584dbf2668 | ||
|  | 9c8976be13 | ||
|  | e08a9cc3a3 | ||
|  | b79a3d6727 | ||
|  | fb96e3588d | ||
|  | edd847ea40 | ||
|  | 83d6834e27 | ||
|  | 8f69d07061 | ||
|  | 30477c764d | ||
|  | 217a80a178 | ||
|  | 5486b40aab | ||
|  | beb8ab50e2 | ||
|  | 7cdf5b55ef | ||
|  | c9b0490305 | ||
|  | d305870284 | ||
|  | ff5004d7db | ||
|  | 7aa3a1a1cc | ||
|  | e124151e5c | ||
|  | e229ed0da3 | ||
|  | 12cdeca48a | ||
|  | a825ef59d4 | ||
|  | 65a5216d17 | ||
|  | 567256bd62 | ||
|  | 4da57c35d0 | ||
|  | f2e8e655ba | ||
|  | 8439232b11 | ||
|  | e6c730ab10 | ||
|  | e49df765d2 | ||
|  | e6da55b925 | ||
|  | c894645747 | ||
|  | 2539cba610 | ||
|  | 5ddbe5cdba | ||
|  | 4c7552eca4 | ||
|  | 72bf0086e4 | ||
|  | 1b91e0027b | ||
|  | e9851e7eb2 | ||
|  | 80fedbc1a5 | ||
|  | a4a71797d9 | ||
|  | 4a97064b2c | ||
|  | a3ef2ed7fd | ||
|  | 3a8b41daa3 | ||
|  | 921be1a17c | ||
|  | e3d673d16c | ||
|  | 39f3f795e2 | ||
|  | 53691d28a8 | ||
|  | 3730b0310b | ||
|  | 2b9013699d | ||
|  | be78827274 | ||
|  | cd1ee96606 | ||
|  | 2fa8d907b3 | ||
|  | 4c383906c4 | ||
|  | bdc6302ea1 | ||
|  | 31c13e4c16 | ||
|  | 6b59f55a50 | ||
|  | e6bd2238ce | ||
|  | 2d4688a206 | ||
|  | 536bcab5de | ||
|  | 1c2d2bce5a | ||
|  | 2eac8b6c46 | ||
|  | 6e50e2aa65 | ||
|  | 841d278224 | ||
|  | 11076e4614 | ||
|  | 72df3d1606 | ||
|  | ae6736311a | ||
|  | c0dcecc465 | ||
|  | d9d368d38e | ||
|  | a70cee1dc1 | ||
|  | f4766ab74f | ||
|  | 4fbf41472a | ||
|  | 6ee02c47c2 | ||
|  | 140d77061b | ||
|  | d6f4f05090 | ||
|  | bdb91112ea | ||
|  | b027b6a711 | ||
|  | 89ecfc2004 | ||
|  | cf835d1580 | ||
|  | 17a09cd221 | ||
|  | 1bd2d41ffd | ||
|  | aa6cea6f7e | ||
|  | ebf895990b | ||
|  | 46a435f5f2 | ||
|  | 6c548a1596 | ||
|  | 7f75f2135d | ||
|  | c49f7293fe | ||
|  | 71496574e9 | ||
|  | b95b4a0694 | ||
|  | 59653ec785 | ||
|  | e02f3cdac7 | ||
|  | d4d630823c | ||
|  | 9fc1377b44 | ||
|  | e3e3d92347 | ||
|  | 13077095c2 | ||
|  | 4001d82ca2 | ||
|  | 4936ca1700 | ||
|  | 2ecd5cff07 | ||
|  | dea297c8d7 | ||
|  | ef7c5c6055 | ||
|  | ee3cfb2b76 | ||
|  | 2cc2a2153b | ||
|  | e51f3d9498 | ||
|  | 1c1f3f7c55 | ||
|  | ea424b0699 | ||
|  | 489d0d20d2 | ||
|  | f04e3de7b8 | ||
|  | a0693060e4 | ||
|  | 888b237964 | ||
|  | 122ff731ef | ||
|  | 3232866dc3 | ||
|  | ccf2854b61 | ||
|  | 03ae6b2c1b | ||
|  | 6bcbbcce02 | ||
|  | fbb9967117 | ||
|  | 6d4f787f67 | ||
|  | 5e27a8df1f | ||
|  | 846b091aac | ||
|  | 372d68a177 | ||
|  | 4fc19902ab | ||
|  | 9a7d5dcad8 | ||
|  | ef78c404dd | ||
|  | c857f98557 | ||
|  | 01a24de3a8 | ||
|  | ae46dcef7e | ||
|  | 872b8ee753 | ||
|  | eb8a2326ad | ||
|  | cf63d627fe | ||
|  | 49e9c43339 | ||
|  | f1dc9537ff | ||
|  | 1ad535d030 | ||
|  | 1ed27b7cc0 | ||
|  | 585586780b | ||
|  | 50aeefc662 | ||
|  | 6e41c22e9d | ||
|  | e81191ebd2 | ||
|  | b29c119408 | ||
|  | e819185de1 | ||
|  | 00465f4a6f | ||
|  | f4dc11477f | ||
|  | 754352b4d7 | ||
|  | 67a4e56fcf | ||
|  | 9bc7b74d01 | ||
|  | 15bfc4c91f | ||
|  | a0159a2746 | ||
|  | 44545a18a0 | ||
|  | 0b51ec2c88 | ||
|  | 5e62c489b0 | ||
|  | d015088855 | ||
|  | 39c889e662 | ||
|  | c7c8711c9c | ||
|  | 0a92405f2d | ||
|  | b4b6b75e84 | ||
|  | a2cab960a9 | ||
|  | 1f7f03f563 | ||
|  | 80226694d5 | ||
|  | 053465d3f6 | ||
|  | 7d75c9157b | ||
|  | b367c01b4b | ||
|  | e6a1254e65 | ||
|  | 1e80c4807e | ||
|  | 928b39f495 | ||
|  | 58d028ac13 | ||
|  | a2dccc4730 | ||
|  | ffee2f0e88 | ||
|  | d885d65c9b | ||
|  | c35240ca32 | ||
|  | 7c00c5db70 | ||
|  | 335faf858b | ||
|  | 1829e68730 | ||
|  | b8eadb2ba5 | ||
|  | 551ea37882 | ||
|  | 3a25eaca3f | ||
|  | e85cbf26f8 | ||
|  | 2ec17eed58 | ||
|  | 2f77d31690 | ||
|  | 3f123d7542 | ||
|  | d189cc1fbe | ||
|  | c0658ffe2c | ||
|  | 248b0bc378 | ||
|  | 80b4c26481 | ||
|  | 5bb4d042e4 | ||
|  | dcc537d0d4 | ||
|  | 2dca3d79e4 | ||
|  | 01497c891d | ||
|  | 77bb46ff3b | ||
|  | cefbfb75bd | ||
|  | 749b942132 | ||
|  | a043022444 | ||
|  | 8b7e061f3a | ||
|  | 74ea1b60e3 | ||
|  | 5a2fed3569 | ||
|  | e85157db4b | ||
|  | d3563e4e97 | ||
|  | 765579dabb | ||
|  | 6afd004ec5 | ||
|  | ee3ee3a63b | ||
|  | aae2ee2ecb | ||
|  | bac6880a1e | ||
|  | 0982ab58ac | ||
|  | 38dd566e0c | ||
|  | 71e1e3b5f8 | ||
|  | abbd7faa64 | ||
|  | aa0e155e22 | ||
|  | 0dab280440 | ||
|  | 90b076eccd | ||
|  | 444c0fc67f | ||
|  | 302ba2874e | ||
|  | df750d0d11 | ||
|  | 63e4d4b493 | ||
|  | 88627095fb | ||
|  | 858d97ccef | ||
|  | 22f30d42a6 | ||
|  | 1e2497748d | ||
|  | 34de2bbe99 | ||
|  | 21cb941bbe | ||
|  | 33fdbbe30c | ||
|  | 09f9d91577 | ||
|  | 34a8eaddb2 | ||
|  | 7dbda12008 | ||
|  | 4101d5dad1 | ||
|  | c20e1975d1 | ||
|  | 4fa3c6915c | ||
|  | ca5c73d170 | ||
|  | 5b5c2fe71b | ||
|  | 9acc21e81a | ||
|  | bff0e81ed3 | ||
|  | 2feffddc55 | ||
|  | 4289e00ad0 | ||
|  | 574ee404d2 | ||
|  | 9caf5f8b31 | ||
|  | 127acfde64 | ||
|  | 156ad773c9 | ||
|  | 8d90d256bf | ||
|  | 833565feb9 | ||
|  | fdebf04196 | ||
|  | dd8d25e43f | ||
|  | 68844c4869 | ||
|  | 7c0543862a | ||
|  | a932ca2f64 | ||
|  | 2597975ae0 | ||
|  | 6330177d24 | ||
|  | 3ac730fb2f | ||
|  | ff48f53989 | ||
|  | 8bb4316956 | ||
|  | 40cdb778f5 | ||
|  | dfd174e1a5 | ||
|  | 735c04cd69 | ||
|  | d95b370998 | ||
|  | 3ebdd62c67 | ||
|  | c26c96b8f4 | ||
|  | 748256b3ee | ||
|  | 10791db82e | ||
|  | 3dd34f6628 | ||
|  | 7004053538 | ||
|  | dc42427c60 | ||
|  | 40ad6befa8 | ||
|  | 612e2c1644 | ||
|  | c8d0cde329 | ||
|  | 5e8794175d | ||
|  | 657527655d | ||
|  | f7543a7b8d | ||
|  | 43a020641b | ||
|  | c019ff34bc | ||
|  | ef6ccddc0d | ||
|  | 8bbe4efded | ||
|  | f490585f66 | ||
|  | fcfc76b01b | ||
|  | 5ad68e926d | ||
|  | 56fa6fef85 | ||
|  | c9e5919739 | ||
|  | 0451b31f9e | ||
|  | 1c845e0ff8 | ||
|  | 22478ffb0f | ||
|  | c38cc128db | ||
|  | fa01149771 | ||
|  | 254522dd93 | ||
|  | 6a86d92781 | ||
|  | b274d6901a | ||
|  | 3ef31e55ca | ||
|  | fb002ac3b0 | ||
|  | de943908bd | ||
|  | b0a25872da | ||
|  | 403d450f47 | ||
|  | d6b96ad51d | ||
|  | 9b4b50a3a6 | ||
|  | 2cca26ada4 | ||
|  | 312799babf | ||
|  | 5bc5a9dcb6 | ||
|  | ef87a6657a | ||
|  | 27e1233fc0 | ||
|  | d24ad2e0e7 | ||
|  | dda27d9de4 | ||
|  | f52136338d | ||
|  | bafb0ad688 | ||
|  | b617b92758 | ||
|  | 39e922580a | ||
|  | 77d0bfc4bb | ||
|  | 654cee6f83 | ||
|  | cf14c02b8a | ||
|  | 42f6095960 | ||
|  | f224984858 | ||
|  | efe4c5e3bc | ||
|  | cedb671f07 | ||
|  | c18bd3ac81 | ||
|  | b08432bd0d | ||
|  | 4bac9707fe | ||
|  | 7e16cda949 | ||
|  | 8a025a6617 | ||
|  | 1c05f5af03 | ||
|  | 4a9d3a3927 | ||
|  | 26694cb55e | ||
|  | 94ad1237ce | ||
|  | 69467ea6ff | ||
|  | 66f500e594 | ||
|  | fc97a6d1e3 | ||
|  | 1a567b6986 | ||
|  | 9211aad524 | ||
|  | 6139b933c5 | ||
|  | 3804b3b759 | ||
|  | 659239e8cd | ||
|  | 52e59d1dad | ||
|  | b8630363e0 | ||
|  | 1d91601094 | ||
|  | ea23f49e90 | ||
|  | b2bf2bc448 | ||
|  | 5ad5ef5a42 | ||
|  | 86a34f4b17 | ||
|  | 6a2ed8241e | ||
|  | 03a95ee05f | ||
|  | 81f6750211 | ||
|  | cbc03aae80 | ||
|  | 390299894e | ||
|  | fc7628cdea | ||
|  | fbd600f43f | ||
|  | 239eadb895 | ||
|  | e87169805c | ||
|  | 9f85d99a22 | ||
|  | 56e305f986 | ||
|  | 1c0ee5ae6b | ||
|  | 949e61db8d | ||
|  | e31a96bfe2 | ||
|  | 6a8e88b1cc | ||
|  | b3cff566eb | ||
|  | 0a62106b7b | ||
|  | 1cf4818640 | ||
|  | 523eedbc51 | ||
|  | e57a1ff42d | ||
|  | 361b6ab961 | ||
|  | 0d80286bb3 | ||
|  | d00e0eb2d6 | ||
|  | 215f26fbe4 | ||
|  | 68928aee7c | ||
|  | ffb0080fc1 | ||
|  | f784e5c9f6 | ||
|  | 748bc85bfe | ||
|  | 050e2547ea | ||
|  | c2518cff89 | ||
|  | 4332301dbb | ||
|  | d5fa17c316 | ||
|  | c1a28ba5e2 | ||
|  | 01e03b76a7 | ||
|  | 507d27e84a | ||
|  | 01f5ca26dc | ||
|  | 20cb2e147f | ||
|  | 49a3d385eb | ||
|  | 023cb4937e | ||
|  | 529ff4bd52 | ||
|  | 3df25a183a | ||
|  | c55b4f5e1b | ||
|  | 3b1b1071f1 | ||
|  | 21fbbc5fb9 | ||
|  | b61577b68b | ||
|  | fa9df32979 | ||
|  | fcce70d416 | ||
|  | 8e54a622d3 | ||
|  | 294fe8d970 | ||
|  | cc53eb42b2 | ||
|  | 4ece4a389e | ||
|  | 2ff863deb3 | ||
|  | 402a6a9edb | ||
|  | af612add24 | ||
|  | 1f8037d5bc | ||
|  | f314ad8a5b | ||
|  | 66f9597d9e | ||
|  | c287673947 | ||
|  | 5f7bde2a2c | ||
|  | 8e5d7337c8 | ||
|  | fb7e7eb80b | ||
|  | 6d24e9ebb5 | ||
|  | ddde64a48d | ||
|  | 446f7e0a7e | ||
|  | d0dc275e30 | ||
|  | 9699719305 | ||
|  | 1596a85e4f | ||
|  | 6fde5e0fed | ||
|  | 34229af38a | ||
|  | 373cb44078 | ||
|  | 3744c7876b | ||
|  | a930b377b0 | ||
|  | 571c0eb827 | ||
|  | 749f664330 | ||
|  | f87d9be60d | ||
|  | 5a3e1d5792 | ||
|  | cb86749545 | ||
|  | 857a3dcf72 | ||
|  | 435789a960 | ||
|  | 18b3fbbf6d | ||
|  | 8bd182d96c | ||
|  | 73e469ae52 | ||
|  | 3835ad8c1f | ||
|  | a63b9a9e0c | ||
|  | 6483ceb6eb | ||
|  | e7fe9b374f | ||
|  | 9014fa4bf9 | ||
|  | bfde7fd9d7 | ||
|  | 3e7161ad41 | ||
|  | 857d79dc71 | ||
|  | f652cd3851 | ||
|  | 5d8fb7cdf4 | ||
|  | 80e3de94d3 | ||
|  | 7f00b5eb65 | ||
|  | de19d25a3c | ||
|  | cf4bfcdce8 | ||
|  | c702a3f3ee | ||
|  | e4c90489f7 | ||
|  | 08c0715a30 | ||
|  | 0df44b5df1 | ||
|  | e315b4d939 | ||
|  | 78d0e0baae | ||
|  | 323c641ecd | ||
|  | 6207510279 | ||
|  | 95a340d7a3 | ||
|  | 6f9e725a2c | ||
|  | 43f6793ad9 | ||
|  | cb7b4d9365 | ||
|  | af1a909c27 | ||
|  | 9ed2ed8177 | ||
|  | 664b219387 | ||
|  | 63cda412f9 | ||
|  | 625726c650 | ||
|  | 955a909846 | ||
|  | e3ae8cd31e | ||
|  | 04248b6840 | ||
|  | dbecade122 | ||
|  | ffc2b58714 | ||
|  | 39ad358b51 | ||
|  | 7b90bfaec6 | ||
|  | 7abbb0fb97 | ||
|  | de7d2c33e1 | ||
|  | c8aed15157 | ||
|  | dcfad31770 | ||
|  | f5c2921b85 | ||
|  | b5e5741ffd | ||
|  | d10feafa9b | ||
|  | 9f42b76de3 | ||
|  | 198bd3b41a | ||
|  | c90dcfc0ca | ||
|  | 7a93dde5d4 | ||
|  | 32995a352b | ||
|  | 9722876ef6 | ||
|  | 8bd46a43b9 | ||
|  | 18a1191e03 | ||
|  | 1548fa0811 | ||
|  | b496233425 | ||
|  | dc4e60526c | ||
|  | 71a7f6383f | ||
|  | e882cea47e | ||
|  | a7fd3b34aa | ||
|  | a96de54d46 | ||
|  | 188faa6530 | ||
|  | 1a71cc3047 | ||
|  | 10ccc5f125 | ||
|  | c6e64a9ed3 | ||
|  | 565835cb59 | ||
|  | f2d8ab62dd | ||
|  | cb4bede6d8 | ||
|  | 39b2f30b16 | ||
|  | e18ae84031 | ||
|  | 5cb1d18574 | ||
|  | f8ec5242c9 | ||
|  | d6eeac0619 | ||
|  | 3b14b0efce | ||
|  | 29f0b504b9 | ||
|  | 01c50432c9 | ||
|  | e5e06a12ef | ||
|  | c9c5ca28d2 | ||
|  | 04ec6c5677 | ||
|  | 816b060edc | ||
|  | 91c7c43682 | ||
|  | ca8e45cf4c | ||
|  | 854bafbd4a | ||
|  | 094c867fba | ||
|  | fc930327b4 | ||
|  | 6490fc9c62 | ||
|  | 3a7aabb2eb | ||
|  | 61223a3cc9 | ||
|  | ca2f25e73b | ||
|  | ba6963cf72 | ||
|  | 721b532d71 | ||
|  | a5d46ae9e5 | ||
|  | f8e8bd2c24 | ||
|  | 69f98e0f87 | ||
|  | 87d801721b | ||
|  | d754bdde1b | ||
|  | 725e50348b | ||
|  | acb00c9c59 | ||
|  | bb6693a255 | ||
|  | c09df3c05d | ||
|  | f28418d0b4 | ||
|  | 1922f2bbee | ||
|  | 0375072bdf | ||
|  | b3f03c07c6 | ||
|  | 4b2032a98e | ||
|  | d6df466237 | ||
|  | 458a8970b6 | ||
|  | 8b6c95f723 | ||
|  | 28eda4b220 | ||
|  | 9975e8b544 | ||
|  | c1774c42c2 | ||
|  | 8677763492 | ||
|  | 388abaf09f | ||
|  | 92ae506ffb | ||
|  | 34cce0e920 | ||
|  | 7e18a5c44f | ||
|  | 5a707b558d | ||
|  | e10f8128c8 | ||
|  | 0f2064193f | ||
|  | dc9c001056 | ||
|  | 60fced53c2 | ||
|  | 71d6bbc7e6 | ||
|  | caaae59ea9 | ||
|  | a01fea54a0 | ||
|  | 43f8f2fd2e | ||
|  | 3c65cabe1d | ||
|  | 5cc8dbace4 | ||
|  | ab620acd4f | ||
|  | 11e155d866 | ||
|  | 68272c39c0 | ||
|  | da72bae94a | ||
|  | 1f21e419aa | ||
|  | 5d4bf5f8e5 | ||
|  | b5a6d3aa9d | ||
|  | 813d517076 | ||
|  | 4ed6a64869 | ||
|  | aaae8f4a87 | ||
|  | 436c6282da | ||
|  | c043bbe598 | ||
|  | 8fae609316 | ||
|  | 848fd0442d | ||
|  | bd3d065a23 | ||
|  | fa497d06b0 | ||
|  | 3cbdf63f56 | ||
|  | 30414667d0 | ||
|  | 1ffee9c4d2 | ||
|  | b425912a80 | ||
|  | 10147d8e0e | ||
|  | c4d225a6f2 | ||
|  | 409e84090e | ||
|  | c96784f591 | ||
|  | 0f82114e64 | ||
|  | 5c7d070307 | ||
|  | 7464b440c0 | ||
|  | baedd74c7a | ||
|  | 8b6d6fe661 | ||
|  | ac9417d469 | ||
|  | 56aa58780d | ||
|  | 75899162b3 | ||
|  | 28bb0ddfeb | ||
|  | e779a09586 | ||
|  | 343650e37d | ||
|  | 2c47eb62a7 | ||
|  | 033ab55206 | ||
|  | e17c7124f4 | ||
|  | e3bfbebb8f | ||
|  | bc20fd57fe | ||
|  | a7167ec3bf | ||
|  | a0c54504cd | ||
|  | c3668b9a4d | ||
|  | 9001d1c0d4 | ||
|  | abb2669f0f | ||
|  | 9713458368 | ||
|  | 5c31ab4060 | ||
|  | 965141fad7 | ||
|  | ecd3d838c9 | ||
|  | ce7adbae99 | ||
|  | 1bc3ccd969 | ||
|  | 5646ec7f9c | ||
|  | 80a0f13722 | ||
|  | fef592b6c6 | ||
|  | b654dea55e | ||
|  | 7133e08755 | ||
|  | 7b233d6871 | ||
|  | 350f17e48f | ||
|  | ccf57488c5 | ||
|  | cf6ea7cb2c | ||
|  | d6f130e35a | ||
|  | 8f09382367 | ||
|  | b2b23f2a4f | ||
|  | 8756b41b63 | ||
|  | 4cb174585c | ||
|  | 56e05998ef | ||
|  | bec2d42c79 | ||
|  | a0eff08f39 | ||
|  | a5fdcb31fc | ||
|  | 1d25db491c | ||
|  | c5b1a8eb81 | ||
|  | 68c56b3e03 | ||
|  | 0c567adf63 | ||
|  | 9ec61cbff3 | ||
|  | 2e58297a16 | ||
|  | f81ce2c707 | ||
|  | 4bd7ba0d30 | ||
|  | 9663b7d67c | ||
|  | b082a64d32 | ||
|  | c9979ad90c | ||
|  | 3598560472 | ||
|  | 506e69addf | ||
|  | 2a70ef05d1 | ||
|  | 8696f922d1 | ||
|  | 2b25daa199 | ||
|  | ab51bbd8f7 | ||
|  | 390d5f2f93 | ||
|  | 8d106e97a2 | ||
|  | fc146dabed | ||
|  | 8d5be27746 | ||
|  | f24fd34d86 | ||
|  | 64ee40d370 | ||
|  | 5f3f106283 | ||
|  | 8148eae134 | ||
|  | f13cf1f7a0 | ||
|  | 8a076cc906 | ||
|  | 82c5cd18de | ||
|  | e769804fe6 | ||
|  | f2e99fa319 | ||
|  | 34d435c996 | ||
|  | d04e706295 | ||
|  | 442e765187 | ||
|  | 15602b0664 | ||
|  | b43c5b851a | ||
|  | a47a17d7e7 | ||
|  | b71c03424e | ||
|  | a3d5b69a9c | ||
|  | 3f1d2c0caf | ||
|  | 7fd65987d3 | ||
|  | 24b6c1d3eb | ||
|  | 9a9757ddeb | ||
|  | 4b91ef5123 | ||
|  | 2a8424a7f2 | ||
|  | 132269c5b8 | ||
|  | ddd8027238 | ||
|  | c348efa401 | ||
|  | 9b0c2234d8 | ||
|  | 73f786c606 | ||
|  | 1e63fddf36 | ||
|  | da0dbe8753 | ||
|  | eccc5a3ea3 | ||
|  | 8667f51cf0 | ||
|  | 455df35e50 | ||
|  | 9188836f70 | ||
|  | b0d9800817 | ||
|  | e6b1780a31 | ||
|  | 71ea2cec1f | ||
|  | 7074fa06ae | ||
|  | 3ba9caa118 | ||
|  | 6b141102d6 | ||
|  | acaec41bb7 | ||
|  | f737ca6e28 | ||
|  | e02319dcff | ||
|  | d18bb34f87 | ||
|  | 87944f0c1b | ||
|  | 38c25dec93 | ||
|  | 81ac9391d1 | ||
|  | 61c6581123 | ||
|  | 4a7570770b | ||
|  | aedfb32482 | ||
|  | a5f18dfe7f | ||
|  | cb9906b921 | ||
|  | 144f1d3663 | ||
|  | 546bfe6db5 | ||
|  | 0af10c58f5 | ||
|  | 5ac9d301ea | ||
|  | a70f926971 | ||
|  | dfacf1bbfe | ||
|  | 3920029aff | ||
|  | 8849443bf6 | ||
|  | dd3dd7a136 | ||
|  | dff6884bed | ||
|  | d7231fadb1 | ||
|  | caa2ea64e3 | ||
|  | 83bb7d0266 | ||
|  | 6e21d79bde | ||
|  | 7c1aa771aa | ||
|  | 12e840ee88 | ||
|  | 25c8676d80 | ||
|  | 341fc65958 | ||
|  | 5b6b7c0d15 | ||
|  | 24515546fd | ||
|  | b3728697cc | ||
|  | e64709c37e | ||
|  | 20c2246533 | ||
|  | acf690c87d | ||
|  | adfec578cf | ||
|  | 39c0019534 | ||
|  | f61582f826 | ||
|  | e343aca9bc | ||
|  | 038f24fcea | ||
|  | d3f2434c57 | ||
|  | 23ffc3ddfb | ||
|  | ad0118dd4a | ||
|  | 7c24f1ba6d | ||
|  | 6e863305aa | ||
|  | 75635956cd | ||
|  | 1f3754684a | ||
|  | da10de9ea8 | ||
|  | 39de179e21 | ||
|  | 2cc14055cf | ||
|  | 19a787c235 | ||
|  | e88e32bf23 | ||
|  | f0d4b5f740 | ||
|  | 2b2a83273f | ||
|  | ae476bb400 | ||
|  | dc24eefe08 | ||
|  | f1aa254e48 | ||
|  | 5d5f3276e9 | ||
|  | 172a358d01 | ||
|  | 0ac549d208 | ||
|  | 8fc42694f6 | ||
|  | 0a7d883633 | ||
|  | 41813b0a1f | ||
|  | 4690e227b8 | ||
|  | 5bec0a6534 | ||
|  | 626ed815fb | ||
|  | 74aee1d453 | ||
|  | d187340fc4 | ||
|  | a464e46d4d | ||
|  | f322ec8f3d | ||
|  | 1f4829598a | ||
|  | 40e79299d5 | ||
|  | 368662969e | ||
|  | fbc830176f | ||
|  | cfb20abb9f | ||
|  | 43b818f2b1 | ||
|  | 32b927de7e | ||
|  | c5b77f4590 | ||
|  | 0fb89d1869 | ||
|  | b32078a5fe | ||
|  | dd20c5eab0 | ||
|  | 0ef73c6dd6 | ||
|  | de0e549187 | ||
|  | e15d0ee150 | ||
|  | 331d556799 | ||
|  | 93e0c71c2f | ||
|  | c512d5ebb6 | ||
|  | f153a7b0fd | ||
|  | 10205e06cb | ||
|  | aa490e3726 | ||
|  | de43c4e6ab | ||
|  | 193db50668 | ||
|  | 659fdefccb | ||
|  | 8980996b1a | ||
|  | 0b3fe73b74 | ||
|  | 4af8230b4f | ||
|  | 0bbefb5b2a | ||
|  | 41baf70660 | ||
|  | eaf2bb70d9 | ||
|  | c910fdf7e5 | ||
|  | f1d19416be | ||
|  | 07b78fea76 | ||
|  | 896af84acc | ||
|  | 44d609b205 | ||
|  | 72cbfd8fea | ||
|  | 71236b170d | ||
|  | bb92ab01d7 | ||
|  | 316a0e1c96 | ||
|  | 0c2f9b9dbb | ||
|  | c6c1d3a3ad | ||
|  | fbab0aceb0 | ||
|  | 54b77a1174 | ||
|  | a34cec217e | ||
|  | 91bb38553d | ||
|  | 531f33a158 | ||
|  | 2d826768b0 | ||
|  | d7f6d4436e | ||
|  | bdd0a36aa3 | ||
|  | 8a89dac5d5 | ||
|  | 8d28c53fd3 | ||
|  | 114476d8b1 | ||
|  | d1bfad9890 | ||
|  | feae794787 | ||
|  | 8a3f0e3b93 | ||
|  | 4a80a09db3 | ||
|  | 7f83bcfdd9 | ||
|  | 99cba0ae7f | ||
|  | 2e8a2fdbd4 | ||
|  | d209a2b45a | ||
|  | d071b05249 | ||
|  | 66b36afe90 | ||
|  | c6b81eff9a | ||
|  | 04b268e319 | ||
|  | 5ac875545f | ||
|  | 6e624ff797 | ||
|  | 08b8ab837a | ||
|  | 1b57d8511b | ||
|  | ee4d5178d6 | ||
|  | dea1e9a1e0 | ||
|  | fa4fbf9d73 | ||
|  | fb6c2aef59 | ||
|  | 6417f1f907 | ||
|  | d1b0e6b5fe | ||
|  | 2f669c99f8 | ||
|  | aa8c963c50 | ||
|  | 2873c6bbaf | ||
|  | 2da939c81c | ||
|  | ee398441b6 | ||
|  | 894d81c577 | ||
|  | 4c6a17e304 | ||
|  | ddaa84683b | ||
|  | dd1e480142 | ||
|  | 6ca7b30f75 | ||
|  | b0a3b5e080 | ||
|  | 5fa54b0885 | ||
|  | 803f3f2e13 | ||
|  | de19588d10 | ||
|  | bc2bc13eb1 | ||
|  | 6c96281a1d | ||
|  | 3727342bce | ||
|  | fc3f806555 | ||
|  | c013c3bf61 | ||
|  | 849a98d5b4 | ||
|  | dd1a72e4d9 | ||
|  | e4e404d54f | ||
|  | ee6f2bfecb | ||
|  | 995db1d0e1 | ||
|  | 5cb80619dd | ||
|  | 0914dc7198 | ||
|  | 12f00a9d3d | ||
|  | 3fb9c93a24 | ||
|  | d8f0dce08f | ||
|  | 5e6c69b930 | ||
|  | 83f9664efb | ||
|  | 582386d3a2 | ||
|  | 7aaa5ce9c8 | ||
|  | 5278ae4b5e | ||
|  | b89dea97d9 | ||
|  | 715184070d | ||
|  | 6294c3b913 | ||
|  | e9cf3623d1 | ||
|  | d0ab2a16a6 | ||
|  | 1f5442f1ba | ||
|  | 7b3d6747d5 | ||
|  | 7904d3b157 | ||
|  | 3a48b10757 | ||
|  | 0e50cac399 | ||
|  | dc4a93f5d0 | ||
|  | e23153d090 | ||
|  | 9a26cdb336 | ||
|  | decf50ed49 | ||
|  | bfdf63055f | ||
|  | cd7894ae8f | ||
|  | 10504c4d68 | ||
|  | 192718fee6 | ||
|  | 855d154439 | ||
|  | 300d48a55e | ||
|  | 7174cf35dd | ||
|  | 0b3145a6df | ||
|  | 04225d5717 | ||
|  | 86791422f0 | ||
|  | 9c2af6318c | ||
|  | c747d7d45d | ||
|  | bbd7c9cf86 | ||
|  | 169fb79c97 | ||
|  | 1579dfeb80 | ||
|  | d8a6d8594a | ||
|  | 7be071a0e9 | ||
|  | 01bcf5fb97 | ||
|  | 91766afb64 | ||
|  | cc4f1c667e | ||
|  | bc26de2d68 | ||
|  | 0179358f9c | ||
|  | d8a5c1ea0c | ||
|  | fb9844463b | ||
|  | 481cf7384a | ||
|  | c9a0daf4b6 | ||
|  | 8a25bedaf9 | ||
|  | 11b8e2e1af | ||
|  | 53cfa8d3a1 | ||
|  | 0262a99274 | ||
|  | 09a947beaa | ||
|  | a6e1ef2dd1 | ||
|  | c5aae8ee25 | ||
|  | 5bd5b777a6 | ||
|  | e39961f7f1 | ||
|  | 0d3cf5cb78 | ||
|  | 96d63de292 | ||
|  | ae2962259e | ||
|  | 7dbc20b776 | ||
|  | a21dab334c | ||
|  | 78450da6f3 | ||
|  | b1868123db | ||
|  | f7af51b92c | ||
|  | 7ee1406f64 | ||
|  | 0f49b58e0a | ||
|  | 17204baac0 | ||
|  | 1e05bcaa61 | ||
|  | 18690d51f5 | ||
|  | 2aacf14e96 | ||
|  | 9c5507ab46 | ||
|  | 0a9703bff9 | ||
|  | 67bd5db6d6 | ||
|  | 6c11f0bd51 | ||
|  | e7556271e7 | ||
|  | 8045b889d3 | ||
|  | 6f074d3692 | ||
|  | b09781afa5 | ||
|  | 1863523cfd | ||
|  | a7a9eb6f71 | ||
|  | c868dae44a | ||
|  | ad8cf69897 | ||
|  | 96f1a146a6 | ||
|  | 775e03cfd9 | ||
|  | 80e5e19956 | ||
|  | 8f16268572 | ||
|  | ba11f2ab0c | ||
|  | 8567877f07 | ||
|  | 310f850ee4 | ||
|  | 896cdab22d | ||
|  | ed6462fa00 | ||
|  | 65b05af014 | ||
|  | c18056bdda | ||
|  | 65a79acfb9 | ||
|  | 18d331d284 | ||
|  | 7d642147c1 | ||
|  | 4c313bc198 | ||
|  | a78b2d0128 | ||
|  | f6848fe24d | ||
|  | a59c9b4f77 | ||
|  | c30913ccde | ||
|  | 41f810f828 | ||
|  | d604c8ae64 | ||
|  | 67d8c7c691 | ||
|  | 015cd42a2e | ||
|  | 51c5d1714c | ||
|  | 1ff302b341 | ||
|  | 6b89763ad6 | ||
|  | 253303f3a9 | ||
|  | d49f2cbec8 | ||
|  | 290816be11 | ||
|  | 2fc43fa9c7 | ||
|  | 5adadeaa07 | ||
|  | 761aae6f89 | ||
|  | b29e1acab8 | ||
|  | 49d4260cfe | ||
|  | c4c46c206f | ||
|  | 8453d9a70d | ||
|  | 68dbf35b09 | ||
|  | 1a242f94db | ||
|  | df52bc3493 | ||
|  | 2044c7e4d4 | ||
|  | b401b5eca8 | ||
|  | 67f41a0c72 | ||
|  | bd7e8fbf86 | 
							
								
								
									
										36
									
								
								.clang-tidy
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								.clang-tidy
									
									
									
									
									
								
							| @@ -7,28 +7,39 @@ Checks: >- | ||||
|   -boost-*, | ||||
|   -bugprone-easily-swappable-parameters, | ||||
|   -bugprone-implicit-widening-of-multiplication-result, | ||||
|   -bugprone-multi-level-implicit-pointer-conversion, | ||||
|   -bugprone-narrowing-conversions, | ||||
|   -bugprone-signed-char-misuse, | ||||
|   -bugprone-switch-missing-default-case, | ||||
|   -cert-dcl50-cpp, | ||||
|   -cert-err33-c, | ||||
|   -cert-err58-cpp, | ||||
|   -cert-oop57-cpp, | ||||
|   -cert-str34-c, | ||||
|   -clang-analyzer-optin.core.EnumCastOutOfRange, | ||||
|   -clang-analyzer-optin.cplusplus.UninitializedObject, | ||||
|   -clang-analyzer-osx.*, | ||||
|   -clang-diagnostic-delete-abstract-non-virtual-dtor, | ||||
|   -clang-diagnostic-delete-non-abstract-non-virtual-dtor, | ||||
|   -clang-diagnostic-deprecated-declarations, | ||||
|   -clang-diagnostic-ignored-optimization-argument, | ||||
|   -clang-diagnostic-missing-field-initializers, | ||||
|   -clang-diagnostic-shadow-field, | ||||
|   -clang-diagnostic-unused-const-variable, | ||||
|   -clang-diagnostic-unused-parameter, | ||||
|   -clang-diagnostic-vla-cxx-extension, | ||||
|   -concurrency-*, | ||||
|   -cppcoreguidelines-avoid-c-arrays, | ||||
|   -cppcoreguidelines-avoid-const-or-ref-data-members, | ||||
|   -cppcoreguidelines-avoid-do-while, | ||||
|   -cppcoreguidelines-avoid-magic-numbers, | ||||
|   -cppcoreguidelines-init-variables, | ||||
|   -cppcoreguidelines-macro-to-enum, | ||||
|   -cppcoreguidelines-macro-usage, | ||||
|   -cppcoreguidelines-missing-std-forward, | ||||
|   -cppcoreguidelines-narrowing-conversions, | ||||
|   -cppcoreguidelines-non-private-member-variables-in-classes, | ||||
|   -cppcoreguidelines-owning-memory, | ||||
|   -cppcoreguidelines-prefer-member-initializer, | ||||
|   -cppcoreguidelines-pro-bounds-array-to-pointer-decay, | ||||
|   -cppcoreguidelines-pro-bounds-constant-array-index, | ||||
| @@ -40,7 +51,9 @@ Checks: >- | ||||
|   -cppcoreguidelines-pro-type-static-cast-downcast, | ||||
|   -cppcoreguidelines-pro-type-union-access, | ||||
|   -cppcoreguidelines-pro-type-vararg, | ||||
|   -cppcoreguidelines-rvalue-reference-param-not-moved, | ||||
|   -cppcoreguidelines-special-member-functions, | ||||
|   -cppcoreguidelines-use-default-member-init, | ||||
|   -cppcoreguidelines-virtual-class-destructor, | ||||
|   -fuchsia-multiple-inheritance, | ||||
|   -fuchsia-overloaded-operator, | ||||
| @@ -60,20 +73,32 @@ Checks: >- | ||||
|   -llvm-include-order, | ||||
|   -llvm-qualified-auto, | ||||
|   -llvmlibc-*, | ||||
|   -misc-non-private-member-variables-in-classes, | ||||
|   -misc-const-correctness, | ||||
|   -misc-include-cleaner, | ||||
|   -misc-no-recursion, | ||||
|   -misc-non-private-member-variables-in-classes, | ||||
|   -misc-unused-parameters, | ||||
|   -modernize-avoid-c-arrays, | ||||
|   -misc-use-anonymous-namespace, | ||||
|   -modernize-avoid-bind, | ||||
|   -modernize-avoid-c-arrays, | ||||
|   -modernize-concat-nested-namespaces, | ||||
|   -modernize-macro-to-enum, | ||||
|   -modernize-return-braced-init-list, | ||||
|   -modernize-type-traits, | ||||
|   -modernize-use-auto, | ||||
|   -modernize-use-constraints, | ||||
|   -modernize-use-default-member-init, | ||||
|   -modernize-use-equals-default, | ||||
|   -modernize-use-trailing-return-type, | ||||
|   -modernize-use-nodiscard, | ||||
|   -modernize-use-nullptr, | ||||
|   -modernize-use-nodiscard, | ||||
|   -modernize-use-nullptr, | ||||
|   -modernize-use-trailing-return-type, | ||||
|   -mpi-*, | ||||
|   -objc-*, | ||||
|   -performance-enum-size, | ||||
|   -readability-avoid-nested-conditional-operator, | ||||
|   -readability-container-contains, | ||||
|   -readability-container-data-pointer, | ||||
|   -readability-convert-member-functions-to-static, | ||||
|   -readability-else-after-return, | ||||
| @@ -82,11 +107,14 @@ Checks: >- | ||||
|   -readability-isolate-declaration, | ||||
|   -readability-magic-numbers, | ||||
|   -readability-make-member-function-const, | ||||
|   -readability-named-parameter, | ||||
|   -readability-redundant-casting, | ||||
|   -readability-redundant-inline-specifier, | ||||
|   -readability-redundant-member-init, | ||||
|   -readability-redundant-string-init, | ||||
|   -readability-uppercase-literal-suffix, | ||||
|   -readability-use-anyofallof, | ||||
| WarningsAsErrors: '*' | ||||
| AnalyzeTemporaryDtors: false | ||||
| FormatStyle:     google | ||||
| CheckOptions: | ||||
|   - key:             google-readability-function-size.StatementThreshold | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| { | ||||
|   "name": "ESPHome Dev", | ||||
|   "image": "ghcr.io/esphome/esphome-lint:dev", | ||||
|   "postCreateCommand": ["script/devcontainer-post-create"], | ||||
|   "postCreateCommand": [ | ||||
|     "script/devcontainer-post-create" | ||||
|   ], | ||||
|   "containerEnv": { | ||||
|     "DEVCONTAINER": "1", | ||||
|     "PIP_BREAK_SYSTEM_PACKAGES": "1", | ||||
| @@ -27,6 +29,9 @@ | ||||
|       "extensions": [ | ||||
|         // python | ||||
|         "ms-python.python", | ||||
|         "ms-python.pylint", | ||||
|         "ms-python.flake8", | ||||
|         "charliermarsh.ruff", | ||||
|         "visualstudioexptteam.vscodeintellicode", | ||||
|         // yaml | ||||
|         "redhat.vscode-yaml", | ||||
| @@ -38,9 +43,18 @@ | ||||
|       "settings": { | ||||
|         "python.languageServer": "Pylance", | ||||
|         "python.pythonPath": "/usr/bin/python3", | ||||
|         "python.linting.pylintEnabled": true, | ||||
|         "python.linting.enabled": true, | ||||
|         "python.formatting.provider": "black", | ||||
|         "pylint.args": [ | ||||
|           "--rcfile=${workspaceFolder}/pyproject.toml" | ||||
|         ], | ||||
|         "flake8.args": [ | ||||
|           "--config=${workspaceFolder}/.flake8" | ||||
|         ], | ||||
|         "ruff.configuration": "${workspaceFolder}/pyproject.toml", | ||||
|         "[python]": { | ||||
|           // VS will say "Value is not accepted" before building the devcontainer, but the warning | ||||
|           // should go away after build is completed. | ||||
|           "editor.defaultFormatter": "charliermarsh.ruff" | ||||
|         }, | ||||
|         "editor.formatOnPaste": false, | ||||
|         "editor.formatOnSave": true, | ||||
|         "editor.formatOnType": true, | ||||
|   | ||||
| @@ -75,6 +75,9 @@ target/ | ||||
| # pyenv | ||||
| .python-version | ||||
|  | ||||
| # asdf | ||||
| .tool-versions | ||||
|  | ||||
| # celery beat schedule file | ||||
| celerybeat-schedule | ||||
|  | ||||
|   | ||||
							
								
								
									
										15
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -7,11 +7,16 @@ | ||||
| - [ ] Bugfix (non-breaking change which fixes an issue) | ||||
| - [ ] New feature (non-breaking change which adds functionality) | ||||
| - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) | ||||
| - [ ] Code quality improvements to existing code or addition of tests | ||||
| - [ ] Other | ||||
|  | ||||
| **Related issue or feature (if applicable):** fixes <link to issue> | ||||
| **Related issue or feature (if applicable):** | ||||
|  | ||||
| **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here> | ||||
| - fixes <link to issue> | ||||
|  | ||||
| **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** | ||||
|  | ||||
| - esphome/esphome-docs#<esphome-docs PR number goes here> | ||||
|  | ||||
| ## Test Environment | ||||
|  | ||||
| @@ -23,12 +28,6 @@ | ||||
| - [ ] RTL87xx | ||||
|  | ||||
| ## Example entry for `config.yaml`: | ||||
| <!-- | ||||
|   Supplying a configuration snippet, makes it easier for a maintainer to test | ||||
|   your PR. Furthermore, for new integrations, it gives an impression of how | ||||
|   the configuration would look like. | ||||
|   Note: Remove this section if this PR does not have an example entry. | ||||
| --> | ||||
|  | ||||
| ```yaml | ||||
| # Example config.yaml | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -46,7 +46,10 @@ runs: | ||||
|  | ||||
|     - name: Build and push to ghcr by digest | ||||
|       id: build-ghcr | ||||
|       uses: docker/build-push-action@v6.0.1 | ||||
|       uses: docker/build-push-action@v6.15.0 | ||||
|       env: | ||||
|         DOCKER_BUILD_SUMMARY: false | ||||
|         DOCKER_BUILD_RECORD_UPLOAD: false | ||||
|       with: | ||||
|         context: . | ||||
|         file: ./docker/Dockerfile | ||||
| @@ -69,7 +72,10 @@ runs: | ||||
|  | ||||
|     - name: Build and push to dockerhub by digest | ||||
|       id: build-dockerhub | ||||
|       uses: docker/build-push-action@v6.0.1 | ||||
|       uses: docker/build-push-action@v6.15.0 | ||||
|       env: | ||||
|         DOCKER_BUILD_SUMMARY: false | ||||
|         DOCKER_BUILD_RECORD_UPLOAD: false | ||||
|       with: | ||||
|         context: . | ||||
|         file: ./docker/Dockerfile | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,12 +17,12 @@ runs: | ||||
|   steps: | ||||
|     - name: Set up Python ${{ inputs.python-version }} | ||||
|       id: python | ||||
|       uses: actions/setup-python@v5.1.0 | ||||
|       uses: actions/setup-python@v5.4.0 | ||||
|       with: | ||||
|         python-version: ${{ inputs.python-version }} | ||||
|     - name: Restore Python virtual environment | ||||
|       id: cache-venv | ||||
|       uses: actions/cache/restore@v4.0.2 | ||||
|       uses: actions/cache/restore@v4.2.2 | ||||
|       with: | ||||
|         path: venv | ||||
|         # yamllint disable-line rule:line-length | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,13 @@ updates: | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|     groups: | ||||
|       docker-actions: | ||||
|         applies-to: version-updates | ||||
|         patterns: | ||||
|           - "docker/setup-qemu-action" | ||||
|           - "docker/login-action" | ||||
|           - "docker/setup-buildx-action" | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/.github/actions/build-image" | ||||
|     schedule: | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							| @@ -21,9 +21,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -33,22 +33,20 @@ concurrency: | ||||
| jobs: | ||||
|   check-docker: | ||||
|     name: Build docker containers | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         os: ["ubuntu-latest", "ubuntu-24.04-arm"] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.6 | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.3.0 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3.0.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|  | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
| @@ -58,6 +56,6 @@ jobs: | ||||
|         run: | | ||||
|           docker/build.py \ | ||||
|             --tag "${TAG}" \ | ||||
|             --arch "${{ matrix.arch }}" \ | ||||
|             --arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \ | ||||
|             --build-type "${{ matrix.build_type }}" \ | ||||
|             build | ||||
|   | ||||
							
								
								
									
										163
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										163
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,9 +9,11 @@ on: | ||||
|     paths: | ||||
|       - "**" | ||||
|       - "!.github/workflows/*.yml" | ||||
|       - "!.github/actions/build-image/*" | ||||
|       - ".github/workflows/ci.yml" | ||||
|       - "!.yamllint" | ||||
|       - "!.github/dependabot.yml" | ||||
|       - "!docker/**" | ||||
|   merge_group: | ||||
|  | ||||
| permissions: | ||||
| @@ -29,23 +31,23 @@ concurrency: | ||||
| jobs: | ||||
|   common: | ||||
|     name: Create common environment | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     outputs: | ||||
|       cache-key: ${{ steps.cache-key.outputs.key }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Generate cache-key | ||||
|         id: cache-key | ||||
|         run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT | ||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||
|         id: python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|       - name: Restore Python virtual environment | ||||
|         id: cache-venv | ||||
|         uses: actions/cache@v4.0.2 | ||||
|         uses: actions/cache@v4.2.2 | ||||
|         with: | ||||
|           path: venv | ||||
|           # yamllint disable-line rule:line-length | ||||
| @@ -59,35 +61,35 @@ jobs: | ||||
|           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|           pip install -e . | ||||
|  | ||||
|   black: | ||||
|     name: Check black | ||||
|     runs-on: ubuntu-latest | ||||
|   ruff: | ||||
|     name: Check ruff | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run black | ||||
|       - name: Run Ruff | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           black --verbose esphome tests | ||||
|           ruff format esphome tests | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   flake8: | ||||
|     name: Check flake8 | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -103,12 +105,12 @@ jobs: | ||||
|  | ||||
|   pylint: | ||||
|     name: Check pylint | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -124,12 +126,12 @@ jobs: | ||||
|  | ||||
|   pyupgrade: | ||||
|     name: Check pyupgrade | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -145,12 +147,12 @@ jobs: | ||||
|  | ||||
|   ci-custom: | ||||
|     name: Run script/ci-custom | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -199,7 +201,7 @@ jobs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -218,18 +220,18 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --cov-report=xml --tb=native tests | ||||
|       - name: Upload coverage to Codecov | ||||
|         uses: codecov/codecov-action@v4 | ||||
|         uses: codecov/codecov-action@v5 | ||||
|         with: | ||||
|           token: ${{ secrets.CODECOV_TOKEN }} | ||||
|  | ||||
|   clang-format: | ||||
|     name: Check clang-format | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -248,78 +250,12 @@ jobs: | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   compile-tests-list: | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       matrix: ${{ steps.set-matrix.outputs.matrix }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|       - name: Find all YAML test files | ||||
|         id: set-matrix | ||||
|         run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   validate-tests: | ||||
|     name: Validate YAML test ${{ matrix.file }} | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|       - compile-tests-list | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run esphome config ${{ matrix.file }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           esphome config ${{ matrix.file }} | ||||
|  | ||||
|   compile-tests: | ||||
|     name: Run YAML test ${{ matrix.file }} | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - compile-tests-list | ||||
|       - validate-tests | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 2 | ||||
|       matrix: | ||||
|         file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run esphome compile ${{ matrix.file }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           esphome compile ${{ matrix.file }} | ||||
|  | ||||
|   clang-tidy: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
| @@ -358,7 +294,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -367,26 +303,30 @@ jobs: | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref == 'refs/heads/dev' | ||||
|         uses: actions/cache@v4.0.2 | ||||
|         uses: actions/cache@v4.2.2 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref != 'refs/heads/dev' | ||||
|         uses: actions/cache/restore@v4.0.2 | ||||
|         uses: actions/cache/restore@v4.2.2 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }} | ||||
|  | ||||
|       - name: Install clang-tidy | ||||
|         run: sudo apt-get install clang-tidy-14 | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|  | ||||
|       - name: Run 'pio run --list-targets -e esp32-idf-tidy' | ||||
|         if: matrix.name == 'Run script/clang-tidy for ESP32 IDF' | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           mkdir -p .temp | ||||
|           pio run --list-targets -e esp32-idf-tidy | ||||
|  | ||||
|       - name: Run clang-tidy | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
| @@ -401,7 +341,7 @@ jobs: | ||||
|         if: always() | ||||
|  | ||||
|   list-components: | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|     if: github.event_name == 'pull_request' | ||||
| @@ -410,7 +350,7 @@ jobs: | ||||
|       count: ${{ steps.list-components.outputs.count }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. | ||||
|           fetch-depth: 500 | ||||
| @@ -443,7 +383,7 @@ jobs: | ||||
|  | ||||
|   test-build-components: | ||||
|     name: Component test ${{ matrix.file }} | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - list-components | ||||
| @@ -455,10 +395,12 @@ jobs: | ||||
|         file: ${{ fromJson(needs.list-components.outputs.components) }} | ||||
|     steps: | ||||
|       - name: Install dependencies | ||||
|         run: sudo apt-get install libsodium-dev libsdl2-dev | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install libsdl2-dev | ||||
|  | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -475,7 +417,7 @@ jobs: | ||||
|  | ||||
|   test-build-components-splitter: | ||||
|     name: Split components for testing into 20 groups maximum | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - list-components | ||||
| @@ -484,7 +426,7 @@ jobs: | ||||
|       matrix: ${{ steps.split.outputs.components }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Split components into 20 groups | ||||
|         id: split | ||||
|         run: | | ||||
| @@ -493,7 +435,7 @@ jobs: | ||||
|  | ||||
|   test-build-components-split: | ||||
|     name: Test split components | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - list-components | ||||
| @@ -509,10 +451,12 @@ jobs: | ||||
|         run: echo ${{ matrix.components }} | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: sudo apt-get install libsodium-dev libsdl2-dev | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install libsdl2-dev | ||||
|  | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
| @@ -527,23 +471,24 @@ jobs: | ||||
|       - name: Compile config | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           mkdir build_cache | ||||
|           export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache | ||||
|           for component in ${{ matrix.components }}; do | ||||
|             ./script/test_build_components -e compile -c $component | ||||
|           done | ||||
|  | ||||
|   ci-status: | ||||
|     name: CI Status | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - compile-tests | ||||
|       - clang-tidy | ||||
|       - list-components | ||||
|       - test-build-components | ||||
|   | ||||
							
								
								
									
										91
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| # | ||||
| # ******** NOTE ******** | ||||
| # We have attempted to detect the languages in your repository. Please check | ||||
| # the `language` matrix defined below to confirm you have the correct set of | ||||
| # supported CodeQL languages. | ||||
| # | ||||
| name: "CodeQL Advanced" | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   schedule: | ||||
|     - cron: "30 18 * * 4" | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze (${{ matrix.language }}) | ||||
|     # Runner size impacts CodeQL analysis time. To learn more, please see: | ||||
|     #   - https://gh.io/recommended-hardware-resources-for-running-codeql | ||||
|     #   - https://gh.io/supported-runners-and-hardware-resources | ||||
|     #   - https://gh.io/using-larger-runners (GitHub.com only) | ||||
|     # Consider using larger runners or machines with greater resources for possible analysis time improvements. | ||||
|     runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} | ||||
|     permissions: | ||||
|       # required for all workflows | ||||
|       security-events: write | ||||
|  | ||||
|       # required to fetch internal or private CodeQL packs | ||||
|       packages: read | ||||
|  | ||||
|       # only required for workflows in private repositories | ||||
|       actions: read | ||||
|       contents: read | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         include: | ||||
|           # - language: c-cpp | ||||
|           #   build-mode: autobuild | ||||
|           - language: python | ||||
|             build-mode: none | ||||
|             # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' | ||||
|             # Use `c-cpp` to analyze code written in C, C++ or both | ||||
|             # Use 'java-kotlin' to analyze code written in Java, Kotlin or both | ||||
|             # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both | ||||
|             # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, | ||||
|             # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. | ||||
|             # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how | ||||
|             # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@v3 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|           build-mode: ${{ matrix.build-mode }} | ||||
|           # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|           # By default, queries listed here will override any specified in a config file. | ||||
|           # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|  | ||||
|           # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs | ||||
|           # queries: security-extended,security-and-quality | ||||
|  | ||||
|       # If the analyze step fails for one of the languages you are analyzing with | ||||
|       # "We were unable to automatically build your code", modify the matrix above | ||||
|       # to set the build mode to "manual" for that language. Then modify this step | ||||
|       # to build your code. | ||||
|       # ℹ️ Command-line programs to run using the OS shell. | ||||
|       # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun | ||||
|       - if: matrix.build-mode == 'manual' | ||||
|         shell: bash | ||||
|         run: | | ||||
|           echo 'If you are using a "manual" build mode for one or more of the' \ | ||||
|             'languages you are analyzing, replace this with the commands to build' \ | ||||
|             'your code, for example:' | ||||
|           echo '  make bootstrap' | ||||
|           echo '  make release' | ||||
|           exit 1 | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@v3 | ||||
|         with: | ||||
|           category: "/language:${{matrix.language}}" | ||||
							
								
								
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "problemMatcher": [ | ||||
|     { | ||||
|       "owner": "black", | ||||
|       "owner": "ruff", | ||||
|       "severity": "error", | ||||
|       "pattern": [ | ||||
|         { | ||||
|           "regexp": "^(.*): (Please format this file with the black formatter)", | ||||
|           "regexp": "^(.*): (Please format this file with the ruff formatter)", | ||||
|           "file": 1, | ||||
|           "message": 2 | ||||
|         } | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,7 @@ jobs: | ||||
|       tag: ${{ steps.tag.outputs.tag }} | ||||
|       branch_build: ${{ steps.tag.outputs.branch_build }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.6 | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         # yamllint disable rule:line-length | ||||
| @@ -51,9 +51,9 @@ jobs: | ||||
|       contents: read | ||||
|       id-token: write | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.6 | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: "3.x" | ||||
|       - name: Set up python environment | ||||
| @@ -65,7 +65,7 @@ jobs: | ||||
|           pip3 install build | ||||
|           python3 -m build | ||||
|       - name: Publish | ||||
|         uses: pypa/gh-action-pypi-publish@v1.9.0 | ||||
|         uses: pypa/gh-action-pypi-publish@v1.12.4 | ||||
|  | ||||
|   deploy-docker: | ||||
|     name: Build ESPHome ${{ matrix.platform }} | ||||
| @@ -80,28 +80,27 @@ jobs: | ||||
|       matrix: | ||||
|         platform: | ||||
|           - linux/amd64 | ||||
|           - linux/arm/v7 | ||||
|           - linux/arm64 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.6 | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.3.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|       - name: Set up QEMU | ||||
|         if: matrix.platform != 'linux/amd64' | ||||
|         uses: docker/setup-qemu-action@v3.0.0 | ||||
|         uses: docker/setup-qemu-action@v3.6.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
| @@ -141,7 +140,7 @@ jobs: | ||||
|           echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Upload digests | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         uses: actions/upload-artifact@v4.6.1 | ||||
|         with: | ||||
|           name: digests-${{ steps.sanitize.outputs.name }} | ||||
|           path: /tmp/digests | ||||
| @@ -174,27 +173,27 @@ jobs: | ||||
|           - ghcr | ||||
|           - dockerhub | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.6 | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4.1.7 | ||||
|         uses: actions/download-artifact@v4.1.9 | ||||
|         with: | ||||
|           pattern: digests-* | ||||
|           path: /tmp/digests | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.3.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         if: matrix.registry == 'dockerhub' | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         if: matrix.registry == 'ghcr' | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9.0.0 | ||||
|       - uses: actions/stale@v9.1.0 | ||||
|         with: | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
| @@ -37,7 +37,7 @@ jobs: | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9.0.0 | ||||
|       - uses: actions/stale@v9.1.0 | ||||
|         with: | ||||
|           days-before-pr-stale: -1 | ||||
|           days-before-pr-close: -1 | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,16 +13,16 @@ jobs: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Checkout Home Assistant | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           repository: home-assistant/core | ||||
|           path: lib/home-assistant | ||||
|  | ||||
|       - name: Setup Python | ||||
|         uses: actions/setup-python@v5.1.0 | ||||
|         uses: actions/setup-python@v5.4.0 | ||||
|         with: | ||||
|           python-version: 3.12 | ||||
|  | ||||
| @@ -36,7 +36,7 @@ jobs: | ||||
|           python ./script/sync-device_class.py | ||||
|  | ||||
|       - name: Commit changes | ||||
|         uses: peter-evans/create-pull-request@v6.0.5 | ||||
|         uses: peter-evans/create-pull-request@v7.0.7 | ||||
|         with: | ||||
|           commit-message: "Synchronise Device Classes from Home Assistant" | ||||
|           committer: esphomebot <esphome@nabucasa.com> | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -18,7 +18,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Run yamllint | ||||
|         uses: frenck/action-yamllint@v1.5.0 | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -75,6 +75,9 @@ cov.xml | ||||
| # pyenv | ||||
| .python-version | ||||
|  | ||||
| # asdf | ||||
| .tool-versions | ||||
|  | ||||
| # Environments | ||||
| .env | ||||
| .venv | ||||
| @@ -138,3 +141,5 @@ sdkconfig.* | ||||
| .tests/ | ||||
|  | ||||
| /components | ||||
| /managed_components | ||||
|  | ||||
|   | ||||
| @@ -2,14 +2,15 @@ | ||||
| # See https://pre-commit.com for more information | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/psf/black-pre-commit-mirror | ||||
|     rev: 24.4.2 | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.9.2 | ||||
|     hooks: | ||||
|       - id: black | ||||
|         args: | ||||
|           - --safe | ||||
|           - --quiet | ||||
|         files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|         args: [--fix] | ||||
|       # Run the formatter. | ||||
|       - id: ruff-format | ||||
|   - repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 6.1.0 | ||||
|     hooks: | ||||
| @@ -44,6 +45,6 @@ repos: | ||||
|     hooks: | ||||
|       - id: pylint | ||||
|         name: pylint | ||||
|         entry: script/run-in-env.sh pylint | ||||
|         language: script | ||||
|         entry: python3 script/run-in-env.py pylint | ||||
|         language: system | ||||
|         types: [python] | ||||
|   | ||||
							
								
								
									
										70
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -24,6 +24,7 @@ esphome/components/ade7953_i2c/* @angelnu | ||||
| esphome/components/ade7953_spi/* @angelnu | ||||
| esphome/components/ads1118/* @solomondg1 | ||||
| esphome/components/ags10/* @mak-42 | ||||
| esphome/components/aic3204/* @kbx81 | ||||
| esphome/components/airthings_ble/* @jeromelaban | ||||
| esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau | ||||
| esphome/components/airthings_wave_mini/* @ncareau | ||||
| @@ -37,6 +38,7 @@ esphome/components/am43/sensor/* @buxtronix | ||||
| esphome/components/analog_threshold/* @ianchi | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| esphome/components/apds9306/* @aodrenah | ||||
| esphome/components/api/* @OttoWinter | ||||
| esphome/components/as5600/* @ammmze | ||||
| esphome/components/as5600/sensor/* @ammmze | ||||
| @@ -45,6 +47,11 @@ esphome/components/async_tcp/* @OttoWinter | ||||
| esphome/components/at581x/* @X-Ryl669 | ||||
| esphome/components/atc_mithermometer/* @ahpohl | ||||
| esphome/components/atm90e26/* @danieltwagner | ||||
| esphome/components/atm90e32/* @circuitsetup @descipher | ||||
| esphome/components/audio/* @kahrendt | ||||
| esphome/components/audio_adc/* @kbx81 | ||||
| esphome/components/audio_dac/* @kbx81 | ||||
| esphome/components/axs15231/* @clydebarrow | ||||
| esphome/components/b_parasite/* @rbaron | ||||
| esphome/components/ballu/* @bazuchan | ||||
| esphome/components/bang_bang/* @OttoWinter | ||||
| @@ -56,15 +63,21 @@ esphome/components/beken_spi_led_strip/* @Mat931 | ||||
| esphome/components/bh1750/* @OttoWinter | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bk72xx/* @kuba2k2 | ||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/bl0942/* @dbuezas | ||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | ||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | ||||
| esphome/components/bluetooth_proxy/* @jesserockz | ||||
| esphome/components/bme280_base/* @esphome/core | ||||
| esphome/components/bme280_spi/* @apbodrov | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/bme68x_bsec2/* @kbx81 @neffs | ||||
| esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs | ||||
| esphome/components/bmi160/* @flaviut | ||||
| esphome/components/bmp280_base/* @ademuri | ||||
| esphome/components/bmp280_i2c/* @ademuri | ||||
| esphome/components/bmp280_spi/* @ademuri | ||||
| esphome/components/bmp3xx/* @latonita | ||||
| esphome/components/bmp3xx_base/* @latonita @martgras | ||||
| esphome/components/bmp3xx_i2c/* @latonita | ||||
| @@ -73,11 +86,14 @@ esphome/components/bmp581/* @kahrendt | ||||
| esphome/components/bp1658cj/* @Cossid | ||||
| esphome/components/bp5758d/* @Cossid | ||||
| esphome/components/button/* @esphome/core | ||||
| esphome/components/bytebuffer/* @clydebarrow | ||||
| esphome/components/canbus/* @danielschramm @mvturnho | ||||
| esphome/components/cap1188/* @mreditor97 | ||||
| esphome/components/captive_portal/* @OttoWinter | ||||
| esphome/components/ccs811/* @habbie | ||||
| esphome/components/cd74hc4067/* @asoehlke | ||||
| esphome/components/ch422g/* @clydebarrow @jesterret | ||||
| esphome/components/chsc6x/* @kkosik20 | ||||
| esphome/components/climate/* @esphome/core | ||||
| esphome/components/climate_ir/* @glmnet | ||||
| esphome/components/color_temperature/* @jesserockz | ||||
| @@ -117,6 +133,10 @@ esphome/components/ens160_base/* @latonita @vincentscode | ||||
| esphome/components/ens160_i2c/* @latonita | ||||
| esphome/components/ens160_spi/* @latonita | ||||
| esphome/components/ens210/* @itn3rd77 | ||||
| esphome/components/es7210/* @kahrendt | ||||
| esphome/components/es7243e/* @kbx81 | ||||
| esphome/components/es8156/* @kbx81 | ||||
| esphome/components/es8311/* @kahrendt @kroimon | ||||
| esphome/components/esp32/* @esphome/core | ||||
| esphome/components/esp32_ble/* @Rapsssito @jesserockz | ||||
| esphome/components/esp32_ble_client/* @jesserockz | ||||
| @@ -129,6 +149,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/ethernet_info/* @gtjadsonsantos | ||||
| esphome/components/event/* @nohat | ||||
| esphome/components/event_emitter/* @Rapsssito | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| esphome/components/ezo/* @ssieb | ||||
| esphome/components/ezo_pmp/* @carlos-sarmiento | ||||
| @@ -143,6 +164,7 @@ esphome/components/ft63x6/* @gpambrozio | ||||
| esphome/components/gcja5/* @gcormier | ||||
| esphome/components/gdk101/* @Szewcson | ||||
| esphome/components/globals/* @esphome/core | ||||
| esphome/components/gp2y1010au0f/* @zry98 | ||||
| esphome/components/gp8403/* @jesserockz | ||||
| esphome/components/gpio/* @esphome/core | ||||
| esphome/components/gpio/one_wire/* @ssieb | ||||
| @@ -150,6 +172,7 @@ esphome/components/gps/* @coogle | ||||
| esphome/components/graph/* @synco | ||||
| esphome/components/graphical_display_menu/* @MrMDavidson | ||||
| esphome/components/gree/* @orestismers | ||||
| esphome/components/grove_gas_mc_v2/* @YorkshireIoT | ||||
| esphome/components/grove_tb6612fng/* @max246 | ||||
| esphome/components/growatt_solar/* @leeuwte | ||||
| esphome/components/gt911/* @clydebarrow @jesserockz | ||||
| @@ -157,15 +180,20 @@ esphome/components/haier/* @paveldn | ||||
| esphome/components/haier/binary_sensor/* @paveldn | ||||
| esphome/components/haier/button/* @paveldn | ||||
| esphome/components/haier/sensor/* @paveldn | ||||
| esphome/components/haier/switch/* @paveldn | ||||
| esphome/components/haier/text_sensor/* @paveldn | ||||
| esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/hbridge/switch/* @dwmw2 | ||||
| esphome/components/he60r/* @clydebarrow | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/hm3301/* @freekode | ||||
| esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/hmac_md5/* @dwmw2 | ||||
| esphome/components/homeassistant/* @OttoWinter @esphome/core | ||||
| esphome/components/homeassistant/number/* @landonr | ||||
| esphome/components/homeassistant/switch/* @Links2004 | ||||
| esphome/components/honeywell_hih_i2c/* @Benichou34 | ||||
| esphome/components/honeywellabp/* @RubyBailey | ||||
| esphome/components/honeywellabp2_i2c/* @jpfaff | ||||
| @@ -179,10 +207,11 @@ esphome/components/htu31d/* @betterengineering | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/hyt271/* @Philippe12 | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/i2c_device/* @gabest11 | ||||
| esphome/components/i2s_audio/* @jesserockz | ||||
| esphome/components/i2s_audio/media_player/* @jesserockz | ||||
| esphome/components/i2s_audio/microphone/* @jesserockz | ||||
| esphome/components/i2s_audio/speaker/* @jesserockz | ||||
| esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt | ||||
| esphome/components/iaqcore/* @yozik04 | ||||
| esphome/components/ili9xxx/* @clydebarrow @nielsnl68 | ||||
| esphome/components/improv_base/* @esphome/core | ||||
| @@ -206,6 +235,7 @@ esphome/components/kuntze/* @ssieb | ||||
| esphome/components/lcd_menu/* @numo68 | ||||
| esphome/components/ld2410/* @regevbr @sebcaps | ||||
| esphome/components/ld2420/* @descipher | ||||
| esphome/components/ld2450/* @hareeshmu | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/libretiny/* @kuba2k2 | ||||
| esphome/components/libretiny_pwm/* @kuba2k2 | ||||
| @@ -214,9 +244,14 @@ esphome/components/lightwaverf/* @max246 | ||||
| esphome/components/lilygo_t5_47/touchscreen/* @jesserockz | ||||
| esphome/components/lock/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/logger/select/* @clydebarrow | ||||
| esphome/components/ltr390/* @latonita @sjtrny | ||||
| esphome/components/ltr501/* @latonita | ||||
| esphome/components/ltr_als_ps/* @latonita | ||||
| esphome/components/lvgl/* @clydebarrow | ||||
| esphome/components/m5stack_8angle/* @rnauber | ||||
| esphome/components/matrix_keypad/* @ssieb | ||||
| esphome/components/max17043/* @blacknell | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
| esphome/components/max6956/* @looping40 | ||||
| @@ -245,6 +280,7 @@ esphome/components/mics_4514/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mixer/speaker/* @kahrendt | ||||
| esphome/components/mlx90393/* @functionpointer | ||||
| esphome/components/mlx90614/* @jesserockz | ||||
| esphome/components/mmc5603/* @benhoff | ||||
| @@ -263,6 +299,8 @@ esphome/components/mopeka_std_check/* @Fabian-Schmidt | ||||
| esphome/components/mpl3115a2/* @kbickar | ||||
| esphome/components/mpu6886/* @fabaff | ||||
| esphome/components/ms8607/* @e28eta | ||||
| esphome/components/msa3xx/* @latonita | ||||
| esphome/components/nau7802/* @cujomalainey | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @edwardtfn @senexcrenshaw | ||||
| esphome/components/nextion/binary_sensor/* @senexcrenshaw | ||||
| @@ -271,8 +309,11 @@ esphome/components/nextion/switch/* @senexcrenshaw | ||||
| esphome/components/nextion/text_sensor/* @senexcrenshaw | ||||
| esphome/components/nfc/* @jesserockz @kbx81 | ||||
| esphome/components/noblex/* @AGalfra | ||||
| esphome/components/npi19/* @bakerkj | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/one_wire/* @ssieb | ||||
| esphome/components/online_image/* @clydebarrow @guillempages | ||||
| esphome/components/opentherm/* @olegtarasov | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/pca6416a/* @Mat931 | ||||
| @@ -300,14 +341,14 @@ esphome/components/pvvx_mithermometer/* @pasiz | ||||
| esphome/components/pylontech/* @functionpointer | ||||
| esphome/components/qmp6988/* @andrewpc | ||||
| esphome/components/qr_code/* @wjtje | ||||
| esphome/components/qspi_amoled/* @clydebarrow | ||||
| esphome/components/qspi_dbi/* @clydebarrow | ||||
| esphome/components/qwiic_pir/* @kahrendt | ||||
| esphome/components/radon_eye_ble/* @jeffeb3 | ||||
| esphome/components/radon_eye_rd200/* @jeffeb3 | ||||
| esphome/components/rc522/* @glmnet | ||||
| esphome/components/rc522_i2c/* @glmnet | ||||
| esphome/components/rc522_spi/* @glmnet | ||||
| esphome/components/resistance_sampler/* @jesserockz | ||||
| esphome/components/resampler/speaker/* @kahrendt | ||||
| esphome/components/restart/* @esphome/core | ||||
| esphome/components/rf_bridge/* @jesserockz | ||||
| esphome/components/rgbct/* @jesserockz | ||||
| @@ -320,10 +361,12 @@ esphome/components/rtttl/* @glmnet | ||||
| esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti | ||||
| esphome/components/scd4x/* @martgras @sjtrny | ||||
| esphome/components/script/* @esphome/core | ||||
| esphome/components/sdl/* @clydebarrow | ||||
| esphome/components/sdl/* @bdm310 @clydebarrow | ||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||
| esphome/components/sdp3x/* @Azimath | ||||
| esphome/components/seeed_mr24hpc1/* @limengdu | ||||
| esphome/components/seeed_mr60bha2/* @limengdu | ||||
| esphome/components/seeed_mr60fda2/* @limengdu | ||||
| esphome/components/selec_meter/* @sourabhjaiswal | ||||
| esphome/components/select/* @esphome/core | ||||
| esphome/components/sen0321/* @notjj | ||||
| @@ -349,7 +392,8 @@ esphome/components/smt100/* @piechade | ||||
| esphome/components/sn74hc165/* @jesserockz | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||
| esphome/components/speaker/* @jesserockz | ||||
| esphome/components/speaker/* @jesserockz @kahrendt | ||||
| esphome/components/speaker/media_player/* @kahrendt @synesthesiam | ||||
| esphome/components/spi/* @clydebarrow @esphome/core | ||||
| esphome/components/spi_device/* @clydebarrow | ||||
| esphome/components/spi_led_strip/* @clydebarrow | ||||
| @@ -373,15 +417,20 @@ esphome/components/st7701s/* @clydebarrow | ||||
| esphome/components/st7735/* @SenexCrenshaw | ||||
| esphome/components/st7789v/* @kbx81 | ||||
| esphome/components/st7920/* @marsjan155 | ||||
| esphome/components/statsd/* @Links2004 | ||||
| esphome/components/substitutions/* @esphome/core | ||||
| esphome/components/sun/* @OttoWinter | ||||
| esphome/components/sun_gtil2/* @Mat931 | ||||
| esphome/components/switch/* @esphome/core | ||||
| esphome/components/switch/binary_sensor/* @ssieb | ||||
| esphome/components/t6615/* @tylermenezes | ||||
| esphome/components/tc74/* @sethgirvan | ||||
| esphome/components/tca9548a/* @andreashergert1984 | ||||
| esphome/components/tca9555/* @mobrembski | ||||
| esphome/components/tcl112/* @glmnet | ||||
| esphome/components/tee501/* @Stock-M | ||||
| esphome/components/teleinfo/* @0hax | ||||
| esphome/components/tem3200/* @bakerkj | ||||
| esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar | ||||
| esphome/components/template/datetime/* @rfdarter | ||||
| esphome/components/template/event/* @nohat | ||||
| @@ -399,6 +448,7 @@ esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp1075/* @sybrenstuvel | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| esphome/components/tormatic/* @ti-mo | ||||
| esphome/components/toshiba/* @kbx81 | ||||
| esphome/components/touchscreen/* @jesserockz @nielsnl68 | ||||
| esphome/components/tsl2591/* @wjcarpenter | ||||
| @@ -412,6 +462,7 @@ esphome/components/tuya/switch/* @jesserockz | ||||
| esphome/components/tuya/text_sensor/* @dentra | ||||
| esphome/components/uart/* @esphome/core | ||||
| esphome/components/uart/button/* @ssieb | ||||
| esphome/components/udp/* @clydebarrow | ||||
| esphome/components/ufire_ec/* @pvizeli | ||||
| esphome/components/ufire_ise/* @pvizeli | ||||
| esphome/components/ultrasonic/* @OttoWinter | ||||
| @@ -424,6 +475,7 @@ esphome/components/veml7700/* @latonita | ||||
| esphome/components/version/* @esphome/core | ||||
| esphome/components/voice_assistant/* @jesserockz | ||||
| esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 | ||||
| esphome/components/watchdog/* @oarcher | ||||
| esphome/components/waveshare_epaper/* @clydebarrow | ||||
| esphome/components/web_server_base/* @OttoWinter | ||||
| esphome/components/web_server_idf/* @dentra | ||||
| @@ -446,11 +498,13 @@ esphome/components/wl_134/* @hobbypunk90 | ||||
| esphome/components/x9c/* @EtienneMD | ||||
| esphome/components/xgzp68xx/* @gcormier | ||||
| esphome/components/xiaomi_hhccjcy10/* @fariouche | ||||
| esphome/components/xiaomi_lywsd02mmc/* @juanluss31 | ||||
| esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||
| esphome/components/xxtea/* @clydebarrow | ||||
| esphome/components/zhlt01/* @cfeenstra1024 | ||||
| esphome/components/zio_ultrasonic/* @kahrendt | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| # Contributing to ESPHome | ||||
| # Contributing to ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) | ||||
|  | ||||
| For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome | ||||
| We welcome contributions to the ESPHome suite of code and documentation! | ||||
|  | ||||
| Things to note when contributing: | ||||
| Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the | ||||
| project and be sure to join us on [Discord](https://discord.gg/KhAMKrd). | ||||
|  | ||||
|  - Please test your changes :) | ||||
|  - If a new feature is added or an existing user-facing feature is changed, you should also | ||||
|    update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs) | ||||
|    for more information. | ||||
|  - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files | ||||
|    which checks if your new feature compiles correctly. | ||||
| **See also:** | ||||
|  | ||||
| [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) | ||||
|  | ||||
| --- | ||||
|  | ||||
| [](https://www.openhomefoundation.org/) | ||||
|   | ||||
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,9 +1,16 @@ | ||||
| # ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) | ||||
|  | ||||
| [](https://esphome.io/) | ||||
| <a href="https://esphome.io/"> | ||||
|   <picture> | ||||
|     <source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo"> | ||||
|     <img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo"> | ||||
|   </picture> | ||||
| </a> | ||||
|  | ||||
| **Documentation:** https://esphome.io/ | ||||
| --- | ||||
|  | ||||
| For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues). | ||||
| [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) | ||||
|  | ||||
| For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues). | ||||
| --- | ||||
|  | ||||
| [](https://www.openhomefoundation.org/) | ||||
|   | ||||
| @@ -29,33 +29,18 @@ RUN \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         python3-pip=23.0.1+dfsg-1 \ | ||||
|         python3-setuptools=66.1.1-1 \ | ||||
|         python3-setuptools=66.1.1-1+deb12u1 \ | ||||
|         python3-venv=3.11.2-1+b1 \ | ||||
|         python3-wheel=0.38.4-2 \ | ||||
|         iputils-ping=3:20221126-1 \ | ||||
|         git=1:2.39.2-1.1 \ | ||||
|         curl=7.88.1-10+deb12u5 \ | ||||
|         openssh-client=1:9.2p1-2+deb12u2 \ | ||||
|         iputils-ping=3:20221126-1+deb12u1 \ | ||||
|         git=1:2.39.5-0+deb12u2 \ | ||||
|         curl=7.88.1-10+deb12u12 \ | ||||
|         openssh-client=1:9.2p1-2+deb12u5 \ | ||||
|         python3-cffi=1.15.1-5 \ | ||||
|         libcairo2=1.16.0-7 \ | ||||
|         libmagic1=1:5.44-3 \ | ||||
|         patch=2.7.6-7; \ | ||||
|     if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         apt-get install -y --no-install-recommends \ | ||||
|           build-essential=12.9 \ | ||||
|           python3-dev=3.11.2-1+b1 \ | ||||
|           zlib1g-dev=1:1.2.13.dfsg-1 \ | ||||
|           libjpeg-dev=1:2.1.5-2 \ | ||||
|           libfreetype-dev=2.12.1+dfsg-5 \ | ||||
|           libssl-dev=3.0.11-1~deb12u2 \ | ||||
|           libffi-dev=3.4.4-1 \ | ||||
|           libopenjp2-7=2.5.0-2 \ | ||||
|           libtiff6=4.5.0-6+deb12u1 \ | ||||
|           cargo=0.66.0+ds1-1 \ | ||||
|           pkg-config=1.8.1-1 \ | ||||
|           gcc-arm-linux-gnueabihf=4:12.2.0-3; \ | ||||
|     fi; \ | ||||
|     rm -rf \ | ||||
|         patch=2.7.6-7 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
| @@ -66,23 +51,11 @@ ENV \ | ||||
|   # Store globally installed pio libs in /piolibs | ||||
|   PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
|  | ||||
| # Support legacy binaries on Debian multiarch system. There is no "correct" way | ||||
| # to do this, other than using properly built toolchains... | ||||
| # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian | ||||
| RUN \ | ||||
|     if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ | ||||
|     fi | ||||
|  | ||||
| RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ | ||||
|     fi; \ | ||||
|     pip3 install \ | ||||
|     --break-system-packages --no-cache-dir \ | ||||
|     # Keep platformio version in sync with requirements.txt | ||||
|     platformio==6.1.15 \ | ||||
|     platformio==6.1.16 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_platformio_interval 1000000 \ | ||||
| @@ -92,14 +65,46 @@ RUN \ | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| # tmpfs is for https://github.com/rust-lang/cargo/issues/8719 | ||||
|  | ||||
| COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / | ||||
| RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ | ||||
|     fi; \ | ||||
|     CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ | ||||
|     pip3 install \ | ||||
|     --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini --libraries | ||||
| COPY requirements.txt requirements_optional.txt / | ||||
| RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN | ||||
| # Fail on any non-zero status | ||||
| set -e | ||||
|  | ||||
| # install build tools in case wheels are not available | ||||
| BUILD_DEPS=" | ||||
|     build-essential=12.9 | ||||
|     python3-dev=3.11.2-1+b1 | ||||
|     zlib1g-dev=1:1.2.13.dfsg-1 | ||||
|     libjpeg-dev=1:2.1.5-2 | ||||
|     libfreetype-dev=2.12.1+dfsg-5+deb12u4 | ||||
|     libssl-dev=3.0.15-1~deb12u1 | ||||
|     libffi-dev=3.4.4-1 | ||||
|     cargo=0.66.0+ds1-1 | ||||
|     pkg-config=1.8.1-1 | ||||
| " | ||||
| LIB_DEPS=" | ||||
|     libtiff6=4.5.0-6+deb12u1 | ||||
|     libopenjp2-7=2.5.0-2+deb12u1 | ||||
| " | ||||
| if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] | ||||
| then | ||||
|     apt-get update | ||||
|     apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS | ||||
| fi | ||||
|  | ||||
| CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo | ||||
| pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt | ||||
|  | ||||
| if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] | ||||
| then | ||||
|     apt-get remove -y --purge --auto-remove $BUILD_DEPS | ||||
|     rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/* | ||||
| fi | ||||
| END-OF-RUN | ||||
|  | ||||
|  | ||||
| COPY script/platformio_install_deps.py platformio.ini / | ||||
| RUN /platformio_install_deps.py /platformio.ini --libraries | ||||
|  | ||||
| # Avoid unsafe git error when container user and file config volume permissions don't match | ||||
| RUN git config --system --add safe.directory '*' | ||||
| @@ -110,11 +115,7 @@ FROM base AS docker | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ | ||||
|   fi; \ | ||||
|   pip3 install \ | ||||
|   --break-system-packages --no-cache-dir -e /esphome | ||||
| RUN pip3 install --break-system-packages --no-cache-dir -e /esphome | ||||
|  | ||||
| # Settings for dashboard | ||||
| ENV USERNAME="" PASSWORD="" | ||||
| @@ -138,6 +139,18 @@ ENTRYPOINT ["/entrypoint.sh"] | ||||
| CMD ["dashboard", "/config"] | ||||
|  | ||||
|  | ||||
| ARG BUILD_VERSION=dev | ||||
|  | ||||
| # Labels | ||||
| LABEL \ | ||||
|     org.opencontainers.image.authors="The ESPHome Authors" \ | ||||
|     org.opencontainers.image.title="ESPHome" \ | ||||
|     org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \ | ||||
|     org.opencontainers.image.url="https://esphome.io/" \ | ||||
|     org.opencontainers.image.documentation="https://esphome.io/" \ | ||||
|     org.opencontainers.image.source="https://github.com/esphome/esphome" \ | ||||
|     org.opencontainers.image.licenses="ESPHome" \ | ||||
|     org.opencontainers.image.version=${BUILD_VERSION} | ||||
|  | ||||
|  | ||||
| # ======================= hassio-type image ======================= | ||||
| @@ -147,7 +160,7 @@ RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         nginx-light=1.22.1-9 \ | ||||
|         nginx-light=1.22.1-9+deb12u1 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -160,16 +173,12 @@ COPY docker/ha-addon-rootfs/ / | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ | ||||
|   fi; \ | ||||
|   pip3 install \ | ||||
|   --break-system-packages --no-cache-dir -e /esphome | ||||
| RUN pip3 install --break-system-packages --no-cache-dir -e /esphome | ||||
|  | ||||
| # Labels | ||||
| LABEL \ | ||||
|     io.hass.name="ESPHome" \ | ||||
|     io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \ | ||||
|     io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \ | ||||
|     io.hass.type="addon" \ | ||||
|     io.hass.version="${BUILD_VERSION}" | ||||
|     # io.hass.arch is inherited from addon-debian-base | ||||
| @@ -184,27 +193,25 @@ ENV \ | ||||
|   PLATFORMIO_CORE_DIR=/esphome/.temp/platformio | ||||
|  | ||||
| RUN \ | ||||
|     apt-get update \ | ||||
|     curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \ | ||||
|     && echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \ | ||||
|     && apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         clang-format-13=1:13.0.1-11+b2 \ | ||||
|         clang-tidy-14=1:14.0.6-12 \ | ||||
|         patch=2.7.6-7 \ | ||||
|         software-properties-common=0.99.30-4 \ | ||||
|         nano=7.2-1 \ | ||||
|         software-properties-common=0.99.30-4.1~deb12u1 \ | ||||
|         nano=7.2-1+deb12u1 \ | ||||
|         build-essential=12.9 \ | ||||
|         python3-dev=3.11.2-1+b1 \ | ||||
|         clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| COPY requirements_test.txt / | ||||
| RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ | ||||
|   fi; \ | ||||
|   pip3 install \ | ||||
|   --break-system-packages --no-cache-dir -r /requirements_test.txt | ||||
| RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt | ||||
|  | ||||
| VOLUME ["/esphome"] | ||||
| WORKDIR /esphome | ||||
|   | ||||
| @@ -1,22 +1,19 @@ | ||||
| #!/usr/bin/env python3 | ||||
| from dataclasses import dataclass | ||||
| import subprocess | ||||
| import argparse | ||||
| from platform import machine | ||||
| import shlex | ||||
| from dataclasses import dataclass | ||||
| import re | ||||
| import shlex | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
|  | ||||
| CHANNEL_DEV = "dev" | ||||
| CHANNEL_BETA = "beta" | ||||
| CHANNEL_RELEASE = "release" | ||||
| CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] | ||||
|  | ||||
| ARCH_AMD64 = "amd64" | ||||
| ARCH_ARMV7 = "armv7" | ||||
| ARCH_AARCH64 = "aarch64" | ||||
| ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] | ||||
| ARCHS = [ARCH_AMD64, ARCH_AARCH64] | ||||
|  | ||||
| TYPE_DOCKER = "docker" | ||||
| TYPE_HA_ADDON = "ha-addon" | ||||
| @@ -76,7 +73,6 @@ class DockerParams: | ||||
|         }[build_type] | ||||
|         platform = { | ||||
|             ARCH_AMD64: "linux/amd64", | ||||
|             ARCH_ARMV7: "linux/arm/v7", | ||||
|             ARCH_AARCH64: "linux/arm64", | ||||
|         }[arch] | ||||
|         target = { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/bin/bash | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| # If /cache is mounted, use that as PIO's coredir | ||||
| # otherwise use path in /config (so that PIO packages aren't downloaded on each compile) | ||||
|   | ||||
| @@ -23,10 +23,6 @@ if bashio::config.true 'streamer_mode'; then | ||||
|     export ESPHOME_STREAMER_MODE=true | ||||
| fi | ||||
|  | ||||
| if bashio::config.true 'status_use_ping'; then | ||||
|     export ESPHOME_DASHBOARD_USE_PING=true | ||||
| fi | ||||
|  | ||||
| if bashio::config.has_value 'relative_url'; then | ||||
|     export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') | ||||
| fi | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| # PYTHON_ARGCOMPLETE_OK | ||||
| import argparse | ||||
| from datetime import datetime | ||||
| import functools | ||||
| import importlib | ||||
| import logging | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
| import time | ||||
| from datetime import datetime | ||||
|  | ||||
| import argcomplete | ||||
|  | ||||
| @@ -20,6 +21,8 @@ from esphome.const import ( | ||||
|     CONF_DEASSERT_RTS_DTR, | ||||
|     CONF_DISABLED, | ||||
|     CONF_ESPHOME, | ||||
|     CONF_LEVEL, | ||||
|     CONF_LOG_TOPIC, | ||||
|     CONF_LOGGER, | ||||
|     CONF_MDNS, | ||||
|     CONF_MQTT, | ||||
| @@ -30,6 +33,7 @@ from esphome.const import ( | ||||
|     CONF_PLATFORMIO_OPTIONS, | ||||
|     CONF_PORT, | ||||
|     CONF_SUBSTITUTIONS, | ||||
|     CONF_TOPIC, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
| @@ -38,15 +42,15 @@ from esphome.const import ( | ||||
|     SECRETS_FILES, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, coroutine | ||||
| from esphome.helpers import indent, is_ip_address | ||||
| from esphome.helpers import get_bool_env, indent, is_ip_address | ||||
| from esphome.log import Fore, color, setup_log | ||||
| from esphome.util import ( | ||||
|     get_serial_ports, | ||||
|     list_yaml_files, | ||||
|     run_external_command, | ||||
|     run_external_process, | ||||
|     safe_print, | ||||
|     list_yaml_files, | ||||
|     get_serial_ports, | ||||
| ) | ||||
| from esphome.log import color, setup_log, Fore | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -63,7 +67,7 @@ def choose_prompt(options, purpose: str = None): | ||||
|         return options[0][1] | ||||
|  | ||||
|     safe_print( | ||||
|         f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' | ||||
|         f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:" | ||||
|     ) | ||||
|     for i, (desc, _) in enumerate(options): | ||||
|         safe_print(f"  [{i + 1}] {desc}") | ||||
| @@ -95,8 +99,12 @@ def choose_upload_log_host( | ||||
|         options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||
|         if default == "OTA": | ||||
|             return CORE.address | ||||
|     if show_mqtt and CONF_MQTT in CORE.config: | ||||
|         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) | ||||
|     if ( | ||||
|         show_mqtt | ||||
|         and (mqtt_config := CORE.config.get(CONF_MQTT)) | ||||
|         and mqtt_logging_enabled(mqtt_config) | ||||
|     ): | ||||
|         options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) | ||||
|         if default == "OTA": | ||||
|             return "MQTT" | ||||
|     if default is not None: | ||||
| @@ -106,6 +114,17 @@ def choose_upload_log_host( | ||||
|     return choose_prompt(options, purpose=purpose) | ||||
|  | ||||
|  | ||||
| def mqtt_logging_enabled(mqtt_config): | ||||
|     log_topic = mqtt_config[CONF_LOG_TOPIC] | ||||
|     if log_topic is None: | ||||
|         return False | ||||
|     if CONF_TOPIC not in log_topic: | ||||
|         return False | ||||
|     if log_topic.get(CONF_LEVEL, None) == "NONE": | ||||
|         return False | ||||
|     return True | ||||
|  | ||||
|  | ||||
| def get_port_type(port): | ||||
|     if port.startswith("/") or port.startswith("COM"): | ||||
|         return "SERIAL" | ||||
| @@ -116,6 +135,7 @@ def get_port_type(port): | ||||
|  | ||||
| def run_miniterm(config, port): | ||||
|     import serial | ||||
|  | ||||
|     from esphome import platformio_api | ||||
|  | ||||
|     if CONF_LOGGER not in config: | ||||
| @@ -317,6 +337,13 @@ def check_permissions(port): | ||||
|  | ||||
|  | ||||
| def upload_program(config, args, host): | ||||
|     try: | ||||
|         module = importlib.import_module("esphome.components." + CORE.target_platform) | ||||
|         if getattr(module, "upload_program")(config, args, host): | ||||
|             return 0 | ||||
|     except AttributeError: | ||||
|         pass | ||||
|  | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         check_permissions(host) | ||||
|         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): | ||||
| @@ -344,7 +371,7 @@ def upload_program(config, args, host): | ||||
|  | ||||
|     from esphome import espota2 | ||||
|  | ||||
|     remote_port = ota_conf[CONF_PORT] | ||||
|     remote_port = int(ota_conf[CONF_PORT]) | ||||
|     password = ota_conf.get(CONF_PASSWORD, "") | ||||
|  | ||||
|     if ( | ||||
| @@ -377,7 +404,7 @@ def show_logs(config, args, port): | ||||
|  | ||||
|             port = mqtt.get_esphome_device_ip( | ||||
|                 config, args.username, args.password, args.client_id | ||||
|             ) | ||||
|             )[0] | ||||
|  | ||||
|         from esphome.components.api.client import run_logs | ||||
|  | ||||
| @@ -596,9 +623,10 @@ def command_update_all(args): | ||||
|  | ||||
|  | ||||
| def command_idedata(args, config): | ||||
|     from esphome import platformio_api | ||||
|     import json | ||||
|  | ||||
|     from esphome import platformio_api | ||||
|  | ||||
|     logging.disable(logging.INFO) | ||||
|     logging.disable(logging.WARNING) | ||||
|  | ||||
| @@ -695,7 +723,8 @@ def command_rename(args, config): | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     os.remove(CORE.config_path) | ||||
|     if CORE.config_path != new_path: | ||||
|         os.remove(CORE.config_path) | ||||
|  | ||||
|     print(color(Fore.BOLD_GREEN, "SUCCESS")) | ||||
|     print() | ||||
| @@ -728,11 +757,23 @@ POST_CONFIG_ACTIONS = { | ||||
| def parse_args(argv): | ||||
|     options_parser = argparse.ArgumentParser(add_help=False) | ||||
|     options_parser.add_argument( | ||||
|         "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" | ||||
|         "-v", | ||||
|         "--verbose", | ||||
|         help="Enable verbose ESPHome logs.", | ||||
|         action="store_true", | ||||
|         default=get_bool_env("ESPHOME_VERBOSE"), | ||||
|     ) | ||||
|     options_parser.add_argument( | ||||
|         "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" | ||||
|     ) | ||||
|     options_parser.add_argument( | ||||
|         "-l", | ||||
|         "--log-level", | ||||
|         help="Set the log level.", | ||||
|         default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"), | ||||
|         action="store", | ||||
|         choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], | ||||
|     ) | ||||
|     options_parser.add_argument( | ||||
|         "--dashboard", help=argparse.SUPPRESS, action="store_true" | ||||
|     ) | ||||
| @@ -746,7 +787,14 @@ def parse_args(argv): | ||||
|     ) | ||||
|  | ||||
|     parser = argparse.ArgumentParser( | ||||
|         description=f"ESPHome v{const.__version__}", parents=[options_parser] | ||||
|         description=f"ESPHome {const.__version__}", parents=[options_parser] | ||||
|     ) | ||||
|  | ||||
|     parser.add_argument( | ||||
|         "--version", | ||||
|         action="version", | ||||
|         version=f"Version: {const.__version__}", | ||||
|         help="Print the ESPHome version and exit.", | ||||
|     ) | ||||
|  | ||||
|     mqtt_options = argparse.ArgumentParser(add_help=False) | ||||
| @@ -947,67 +995,6 @@ def parse_args(argv): | ||||
|     # a deprecation warning). | ||||
|     arguments = argv[1:] | ||||
|  | ||||
|     # On Python 3.9+ we can simply set exit_on_error=False in the constructor | ||||
|     def _raise(x): | ||||
|         raise argparse.ArgumentError(None, x) | ||||
|  | ||||
|     # First, try new-style parsing, but don't exit in case of failure | ||||
|     try: | ||||
|         # duplicate parser so that we can use the original one to raise errors later on | ||||
|         current_parser = argparse.ArgumentParser(add_help=False, parents=[parser]) | ||||
|         current_parser.set_defaults(deprecated_argv_suggestion=None) | ||||
|         current_parser.error = _raise | ||||
|         return current_parser.parse_args(arguments) | ||||
|     except argparse.ArgumentError: | ||||
|         pass | ||||
|  | ||||
|     # Second, try compat parsing and rearrange the command-line if it succeeds | ||||
|     # Disable argparse's built-in help option and add it manually to prevent this | ||||
|     # parser from printing the help messagefor the old format when invoked with -h. | ||||
|     compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) | ||||
|     compat_parser.add_argument("-h", "--help", action="store_true") | ||||
|     compat_parser.add_argument("configuration", nargs="*") | ||||
|     compat_parser.add_argument( | ||||
|         "command", | ||||
|         choices=[ | ||||
|             "config", | ||||
|             "compile", | ||||
|             "upload", | ||||
|             "logs", | ||||
|             "run", | ||||
|             "clean-mqtt", | ||||
|             "wizard", | ||||
|             "mqtt-fingerprint", | ||||
|             "version", | ||||
|             "clean", | ||||
|             "dashboard", | ||||
|             "vscode", | ||||
|             "update-all", | ||||
|         ], | ||||
|     ) | ||||
|  | ||||
|     try: | ||||
|         compat_parser.error = _raise | ||||
|         result, unparsed = compat_parser.parse_known_args(argv[1:]) | ||||
|         last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration) | ||||
|         unparsed = [ | ||||
|             "--device" if arg in ("--upload-port", "--serial-port") else arg | ||||
|             for arg in unparsed | ||||
|         ] | ||||
|         arguments = ( | ||||
|             arguments[0:last_option] | ||||
|             + [result.command] | ||||
|             + result.configuration | ||||
|             + unparsed | ||||
|         ) | ||||
|         deprecated_argv_suggestion = arguments | ||||
|     except argparse.ArgumentError: | ||||
|         # old-style parsing failed, don't suggest any argument | ||||
|         deprecated_argv_suggestion = None | ||||
|  | ||||
|     # Finally, run the new-style parser again with the possibly swapped arguments, | ||||
|     # and let it error out if the command is unparsable. | ||||
|     parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) | ||||
|     argcomplete.autocomplete(parser) | ||||
|     return parser.parse_args(arguments) | ||||
|  | ||||
| @@ -1016,26 +1003,17 @@ def run_esphome(argv): | ||||
|     args = parse_args(argv) | ||||
|     CORE.dashboard = args.dashboard | ||||
|  | ||||
|     setup_log( | ||||
|         args.verbose, | ||||
|         args.quiet, | ||||
|         # Show timestamp for dashboard access logs | ||||
|         args.command == "dashboard", | ||||
|     ) | ||||
|     if args.deprecated_argv_suggestion is not None and args.command != "vscode": | ||||
|         _LOGGER.warning( | ||||
|             "Calling ESPHome with the configuration before the command is deprecated " | ||||
|             "and will be removed in the future. " | ||||
|         ) | ||||
|         _LOGGER.warning("Please instead use:") | ||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion)) | ||||
|     # Override log level if verbose is set | ||||
|     if args.verbose: | ||||
|         args.log_level = "DEBUG" | ||||
|     elif args.quiet: | ||||
|         args.log_level = "CRITICAL" | ||||
|  | ||||
|     if sys.version_info < (3, 8, 0): | ||||
|         _LOGGER.error( | ||||
|             "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " | ||||
|             "with this Python version. Please reinstall ESPHome with Python 3.8+" | ||||
|         ) | ||||
|         return 1 | ||||
|     setup_log( | ||||
|         log_level=args.log_level, | ||||
|         # Show timestamp for dashboard access logs | ||||
|         include_timestamp=args.command == "dashboard", | ||||
|     ) | ||||
|  | ||||
|     if args.command in PRE_CONFIG_ACTIONS: | ||||
|         try: | ||||
|   | ||||
| @@ -1,16 +1,18 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ALL, | ||||
|     CONF_ANY, | ||||
|     CONF_AUTOMATION_ID, | ||||
|     CONF_CONDITION, | ||||
|     CONF_COUNT, | ||||
|     CONF_ELSE, | ||||
|     CONF_ID, | ||||
|     CONF_THEN, | ||||
|     CONF_TIME, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE_ID, | ||||
|     CONF_TIME, | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| @@ -73,6 +75,13 @@ def validate_potentially_and_condition(value): | ||||
|     return validate_condition(value) | ||||
|  | ||||
|  | ||||
| def validate_potentially_or_condition(value): | ||||
|     if isinstance(value, list): | ||||
|         with cv.remove_prepend_path(["or"]): | ||||
|             return validate_condition({"or": value}) | ||||
|     return validate_condition(value) | ||||
|  | ||||
|  | ||||
| DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| @@ -166,6 +175,18 @@ async def or_condition_to_code(config, condition_id, template_arg, args): | ||||
|     return cg.new_Pvariable(condition_id, template_arg, conditions) | ||||
|  | ||||
|  | ||||
| @register_condition("all", AndCondition, validate_condition_list) | ||||
| async def all_condition_to_code(config, condition_id, template_arg, args): | ||||
|     conditions = await build_condition_list(config, template_arg, args) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, conditions) | ||||
|  | ||||
|  | ||||
| @register_condition("any", OrCondition, validate_condition_list) | ||||
| async def any_condition_to_code(config, condition_id, template_arg, args): | ||||
|     conditions = await build_condition_list(config, template_arg, args) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, conditions) | ||||
|  | ||||
|  | ||||
| @register_condition("not", NotCondition, validate_potentially_and_condition) | ||||
| async def not_condition_to_code(config, condition_id, template_arg, args): | ||||
|     condition = await build_condition(config, template_arg, args) | ||||
| @@ -223,15 +244,21 @@ async def delay_action_to_code(config, action_id, template_arg, args): | ||||
|     IfAction, | ||||
|     cv.All( | ||||
|         { | ||||
|             cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|             cv.Exclusive( | ||||
|                 CONF_CONDITION, CONF_CONDITION | ||||
|             ): validate_potentially_and_condition, | ||||
|             cv.Exclusive(CONF_ANY, CONF_CONDITION): validate_potentially_or_condition, | ||||
|             cv.Exclusive(CONF_ALL, CONF_CONDITION): validate_potentially_and_condition, | ||||
|             cv.Optional(CONF_THEN): validate_action_list, | ||||
|             cv.Optional(CONF_ELSE): validate_action_list, | ||||
|         }, | ||||
|         cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), | ||||
|         cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), | ||||
|     ), | ||||
| ) | ||||
| async def if_action_to_code(config, action_id, template_arg, args): | ||||
|     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) | ||||
|     cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) | ||||
|     conditions = await build_condition(config[cond_conf], template_arg, args) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, conditions) | ||||
|     if CONF_THEN in config: | ||||
|         actions = await build_action_list(config[CONF_THEN], template_arg, args) | ||||
|   | ||||
| @@ -8,85 +8,86 @@ | ||||
| # want to break suddenly due to a rename (this file will get backports for features). | ||||
|  | ||||
| # pylint: disable=unused-import | ||||
| from esphome.cpp_generator import (  # noqa | ||||
| from esphome.cpp_generator import (  # noqa: F401 | ||||
|     ArrayInitializer, | ||||
|     Expression, | ||||
|     LineComment, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     Pvariable, | ||||
|     RawExpression, | ||||
|     RawStatement, | ||||
|     TemplateArguments, | ||||
|     StructInitializer, | ||||
|     ArrayInitializer, | ||||
|     safe_exp, | ||||
|     Statement, | ||||
|     LineComment, | ||||
|     progmem_array, | ||||
|     static_const_array, | ||||
|     statement, | ||||
|     variable, | ||||
|     with_local_variable, | ||||
|     new_variable, | ||||
|     Pvariable, | ||||
|     new_Pvariable, | ||||
|     StructInitializer, | ||||
|     TemplateArguments, | ||||
|     add, | ||||
|     add_global, | ||||
|     add_library, | ||||
|     add_build_flag, | ||||
|     add_define, | ||||
|     add_global, | ||||
|     add_library, | ||||
|     add_platformio_option, | ||||
|     get_variable, | ||||
|     get_variable_with_full_id, | ||||
|     process_lambda, | ||||
|     is_template, | ||||
|     new_Pvariable, | ||||
|     new_variable, | ||||
|     process_lambda, | ||||
|     progmem_array, | ||||
|     safe_exp, | ||||
|     statement, | ||||
|     static_const_array, | ||||
|     templatable, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     variable, | ||||
|     with_local_variable, | ||||
| ) | ||||
| from esphome.cpp_helpers import (  # noqa | ||||
|     gpio_pin_expression, | ||||
|     register_component, | ||||
| from esphome.cpp_helpers import (  # noqa: F401 | ||||
|     build_registry_entry, | ||||
|     build_registry_list, | ||||
|     extract_registry_entry_config, | ||||
|     register_parented, | ||||
|     gpio_pin_expression, | ||||
|     past_safe_mode, | ||||
|     register_component, | ||||
|     register_parented, | ||||
| ) | ||||
| from esphome.cpp_types import (  # noqa | ||||
|     global_ns, | ||||
|     void, | ||||
|     nullptr, | ||||
|     float_, | ||||
|     double, | ||||
| from esphome.cpp_types import (  # noqa: F401 | ||||
|     NAN, | ||||
|     App, | ||||
|     Application, | ||||
|     Component, | ||||
|     ComponentPtr, | ||||
|     Controller, | ||||
|     EntityBase, | ||||
|     EntityCategory, | ||||
|     ESPTime, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     JsonObject, | ||||
|     JsonObjectConst, | ||||
|     Parented, | ||||
|     PollingComponent, | ||||
|     arduino_json_ns, | ||||
|     bool_, | ||||
|     const_char_ptr, | ||||
|     double, | ||||
|     esphome_ns, | ||||
|     float_, | ||||
|     global_ns, | ||||
|     gpio_Flags, | ||||
|     int16, | ||||
|     int32, | ||||
|     int64, | ||||
|     int_, | ||||
|     nullptr, | ||||
|     optional, | ||||
|     size_t, | ||||
|     std_ns, | ||||
|     std_shared_ptr, | ||||
|     std_string, | ||||
|     std_string_ref, | ||||
|     std_vector, | ||||
|     uint8, | ||||
|     uint16, | ||||
|     uint32, | ||||
|     uint64, | ||||
|     int16, | ||||
|     int32, | ||||
|     int64, | ||||
|     size_t, | ||||
|     const_char_ptr, | ||||
|     NAN, | ||||
|     esphome_ns, | ||||
|     App, | ||||
|     EntityBase, | ||||
|     Component, | ||||
|     ComponentPtr, | ||||
|     PollingComponent, | ||||
|     Application, | ||||
|     optional, | ||||
|     arduino_json_ns, | ||||
|     JsonObject, | ||||
|     JsonObjectConst, | ||||
|     Controller, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     gpio_Flags, | ||||
|     EntityCategory, | ||||
|     Parented, | ||||
|     ESPTime, | ||||
|     void, | ||||
| ) | ||||
|   | ||||
| @@ -1,11 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER | ||||
|  | ||||
| from esphome.core import CORE | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.const import PLATFORM_ESP8266 | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
| @@ -15,6 +10,9 @@ from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| @@ -38,6 +36,14 @@ ATTENUATION_MODES = { | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| sampling_mode = adc_ns.enum("SamplingMode", is_class=True) | ||||
|  | ||||
| SAMPLING_MODES = { | ||||
|     "avg": sampling_mode.AVG, | ||||
|     "min": sampling_mode.MIN, | ||||
|     "max": sampling_mode.MAX, | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") | ||||
|  | ||||
| @@ -102,11 +108,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,332 +0,0 @@ | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
| #include <Esp.h> | ||||
| ADC_MODE(ADC_VCC) | ||||
| #else | ||||
| #include <Arduino.h> | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
| #include "pico/cyw43_arch.h" | ||||
| #endif | ||||
| #include <hardware/adc.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
|  | ||||
| // 13-bit for S2, 12-bit for all other ESP32 variants | ||||
| #ifdef USE_ESP32 | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit) | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit) | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| extern "C" | ||||
| #endif | ||||
|     void | ||||
|     ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040) | ||||
|   this->pin_->setup(); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|     if (!this->autorange_) { | ||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     if (!this->autorange_) { | ||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // load characteristics for each attenuation | ||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { | ||||
|     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; | ||||
|     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, | ||||
|                                               1100,  // default vref | ||||
|                                               &this->cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: | ||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: | ||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   static bool initialized = false; | ||||
|   if (!initialized) { | ||||
|     adc_init(); | ||||
|     initialized = true; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str()); | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
| #if defined(USE_ESP8266) || defined(USE_LIBRETINY) | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| #endif | ||||
| #endif  // USE_ESP8266 || USE_LIBRETINY | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   if (this->autorange_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Attenuation: auto"); | ||||
|   } else { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 0db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 2.5db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 6db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_12_COMPAT: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 12db"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   if (this->is_temperature_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Pin: Temperature"); | ||||
|   } else { | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|     ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else | ||||
|     LOG_PIN("  Pin: ", this->pin_); | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|   } | ||||
| #endif  // USE_RP2040 | ||||
|   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
| void ADCSensor::update() { | ||||
|   float value_v = this->sample(); | ||||
|   ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); | ||||
|   this->publish_state(value_v); | ||||
| } | ||||
|  | ||||
| void ADCSensor::set_sample_count(uint8_t sample_count) { | ||||
|   if (sample_count != 0) { | ||||
|     this->sample_count_ = sample_count; | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| float ADCSensor::sample() { | ||||
|   uint32_t raw = 0; | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|     raw += ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance) | ||||
| #else | ||||
|     raw += analogRead(this->pin_->get_pin());  // NOLINT | ||||
| #endif | ||||
|   } | ||||
|   raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|   if (this->output_raw_) { | ||||
|     return raw; | ||||
|   } | ||||
|   return raw / 1024.0f; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| float ADCSensor::sample() { | ||||
|   if (!this->autorange_) { | ||||
|     uint32_t sum = 0; | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       int raw = -1; | ||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|         raw = adc1_get_raw(this->channel1_); | ||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); | ||||
|       } | ||||
|       if (raw == -1) { | ||||
|         return NAN; | ||||
|       } | ||||
|       sum += raw; | ||||
|     } | ||||
|     sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|     if (this->output_raw_) { | ||||
|       return sum; | ||||
|     } | ||||
|     uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     raw12 = adc1_get_raw(this->channel1_); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); | ||||
|       raw6 = adc1_get_raw(this->channel1_); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); | ||||
|         raw2 = adc1_get_raw(this->channel1_); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); | ||||
|           raw0 = adc1_get_raw(this->channel1_); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | ||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); | ||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw12 == -1) { | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) | ||||
|   uint32_t c12 = std::min(raw12, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   // max theoretical csum value is 4096*4 = 16384 | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|  | ||||
|   // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 | ||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| float ADCSensor::sample() { | ||||
|   if (this->is_temperature_) { | ||||
|     adc_set_temp_sensor_enabled(true); | ||||
|     delay(1); | ||||
|     adc_select_input(4); | ||||
|     uint32_t raw = 0; | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       raw += adc_read(); | ||||
|     } | ||||
|     raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|     adc_set_temp_sensor_enabled(false); | ||||
|     if (this->output_raw_) { | ||||
|       return raw; | ||||
|     } | ||||
|     return raw * 3.3f / 4096.0f; | ||||
|   } else { | ||||
|     uint8_t pin = this->pin_->get_pin(); | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
|     if (pin == PICO_VSYS_PIN) { | ||||
|       // Measuring VSYS on Raspberry Pico W needs to be wrapped with | ||||
|       // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in | ||||
|       // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and | ||||
|       // VSYS ADC both share GPIO29 | ||||
|       cyw43_thread_enter(); | ||||
|     } | ||||
| #endif  // CYW43_USES_VSYS_PIN | ||||
|  | ||||
|     adc_gpio_init(pin); | ||||
|     adc_select_input(pin - 26); | ||||
|  | ||||
|     uint32_t raw = 0; | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       raw += adc_read(); | ||||
|     } | ||||
|     raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|  | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
|     if (pin == PICO_VSYS_PIN) { | ||||
|       cyw43_thread_exit(); | ||||
|     } | ||||
| #endif  // CYW43_USES_VSYS_PIN | ||||
|  | ||||
|     if (this->output_raw_) { | ||||
|       return raw; | ||||
|     } | ||||
|     float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0; | ||||
|     return raw * 3.3f / 4096.0f * coeff; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIBRETINY | ||||
| float ADCSensor::sample() { | ||||
|   uint32_t raw = 0; | ||||
|   if (this->output_raw_) { | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       raw += analogRead(this->pin_->get_pin());  // NOLINT | ||||
|     } | ||||
|     raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|     return raw; | ||||
|   } | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     raw += analogReadVoltage(this->pin_->get_pin());  // NOLINT | ||||
|   } | ||||
|   raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|   return raw / 1000.0f; | ||||
| } | ||||
| #endif  // USE_LIBRETINY | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
| #endif | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
| @@ -3,13 +3,12 @@ | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include <esp_adc_cal.h> | ||||
| #include "driver/adc.h" | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
| @@ -29,6 +28,21 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; | ||||
| const LogString *sampling_mode_to_str(SamplingMode mode); | ||||
|  | ||||
| class Aggregator { | ||||
|  public: | ||||
|   void add_sample(uint32_t value); | ||||
|   uint32_t aggregate(); | ||||
|   Aggregator(SamplingMode mode); | ||||
|  | ||||
|  protected: | ||||
|   SamplingMode mode_{SamplingMode::AVG}; | ||||
|   uint32_t aggr_{0}; | ||||
|   uint32_t samples_{0}; | ||||
| }; | ||||
|  | ||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||
|  public: | ||||
| #ifdef USE_ESP32 | ||||
| @@ -43,7 +57,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|     this->channel1_ = ADC1_CHANNEL_MAX; | ||||
|   } | ||||
|   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
|   /// Update ADC values | ||||
|   void update() override; | ||||
| @@ -55,24 +69,26 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } | ||||
|   void set_sample_count(uint8_t sample_count); | ||||
|   void set_sampling_mode(SamplingMode sampling_mode); | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
|   std::string unique_id() override; | ||||
| #endif | ||||
| #endif  // USE_ESP8266 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   void set_is_temperature() { this->is_temperature_ = true; } | ||||
| #endif | ||||
| #endif  // USE_RP2040 | ||||
|  | ||||
|  protected: | ||||
|   InternalGPIOPin *pin_; | ||||
|   bool output_raw_{false}; | ||||
|   uint8_t sample_count_{1}; | ||||
|   SamplingMode sampling_mode_{SamplingMode::AVG}; | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   bool is_temperature_{false}; | ||||
| #endif | ||||
| #endif  // USE_RP2040 | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
| @@ -83,8 +99,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; | ||||
| #else | ||||
|   esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; | ||||
| #endif | ||||
| #endif | ||||
| #endif  // ESP_IDF_VERSION_MAJOR | ||||
| #endif  // USE_ESP32 | ||||
| }; | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
							
								
								
									
										79
									
								
								esphome/components/adc/adc_sensor_common.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								esphome/components/adc/adc_sensor_common.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.common"; | ||||
|  | ||||
| const LogString *sampling_mode_to_str(SamplingMode mode) { | ||||
|   switch (mode) { | ||||
|     case SamplingMode::AVG: | ||||
|       return LOG_STR("average"); | ||||
|     case SamplingMode::MIN: | ||||
|       return LOG_STR("minimum"); | ||||
|     case SamplingMode::MAX: | ||||
|       return LOG_STR("maximum"); | ||||
|   } | ||||
|   return LOG_STR("unknown"); | ||||
| } | ||||
|  | ||||
| Aggregator::Aggregator(SamplingMode mode) { | ||||
|   this->mode_ = mode; | ||||
|   // set to max uint if mode is "min" | ||||
|   if (mode == SamplingMode::MIN) { | ||||
|     this->aggr_ = UINT32_MAX; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Aggregator::add_sample(uint32_t value) { | ||||
|   this->samples_ += 1; | ||||
|  | ||||
|   switch (this->mode_) { | ||||
|     case SamplingMode::AVG: | ||||
|       this->aggr_ += value; | ||||
|       break; | ||||
|  | ||||
|     case SamplingMode::MIN: | ||||
|       if (value < this->aggr_) { | ||||
|         this->aggr_ = value; | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case SamplingMode::MAX: | ||||
|       if (value > this->aggr_) { | ||||
|         this->aggr_ = value; | ||||
|       } | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint32_t Aggregator::aggregate() { | ||||
|   if (this->mode_ == SamplingMode::AVG) { | ||||
|     if (this->samples_ == 0) { | ||||
|       return this->aggr_; | ||||
|     } | ||||
|  | ||||
|     return (this->aggr_ + (this->samples_ >> 1)) / this->samples_;  // NOLINT(clang-analyzer-core.DivideZero) | ||||
|   } | ||||
|  | ||||
|   return this->aggr_; | ||||
| } | ||||
|  | ||||
| void ADCSensor::update() { | ||||
|   float value_v = this->sample(); | ||||
|   ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); | ||||
|   this->publish_state(value_v); | ||||
| } | ||||
|  | ||||
| void ADCSensor::set_sample_count(uint8_t sample_count) { | ||||
|   if (sample_count != 0) { | ||||
|     this->sample_count_ = sample_count; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } | ||||
|  | ||||
| float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
							
								
								
									
										166
									
								
								esphome/components/adc/adc_sensor_esp32.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								esphome/components/adc/adc_sensor_esp32.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.esp32"; | ||||
|  | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 | ||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|     if (!this->autorange_) { | ||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     if (!this->autorange_) { | ||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { | ||||
|     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; | ||||
|     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, | ||||
|                                               1100,  // default vref | ||||
|                                               &this->cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: | ||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: | ||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   if (this->autorange_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Attenuation: auto"); | ||||
|   } else { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 0db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 2.5db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 6db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_12_COMPAT: | ||||
|         ESP_LOGCONFIG(TAG, "  Attenuation: 12db"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_); | ||||
|   ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   if (!this->autorange_) { | ||||
|     auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       int raw = -1; | ||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|         raw = adc1_get_raw(this->channel1_); | ||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); | ||||
|       } | ||||
|       if (raw == -1) { | ||||
|         return NAN; | ||||
|       } | ||||
|  | ||||
|       aggr.add_sample(raw); | ||||
|     } | ||||
|     if (this->output_raw_) { | ||||
|       return aggr.aggregate(); | ||||
|     } | ||||
|     uint32_t mv = | ||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     raw12 = adc1_get_raw(this->channel1_); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); | ||||
|       raw6 = adc1_get_raw(this->channel1_); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); | ||||
|         raw2 = adc1_get_raw(this->channel1_); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); | ||||
|           raw0 = adc1_get_raw(this->channel1_); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | ||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); | ||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw12 == -1) { | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   uint32_t c12 = std::min(raw12, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|  | ||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										62
									
								
								esphome/components/adc/adc_sensor_esp8266.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/adc/adc_sensor_esp8266.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| #ifdef USE_ESP8266 | ||||
|  | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
| #include <Esp.h> | ||||
| ADC_MODE(ADC_VCC) | ||||
| #else | ||||
| #include <Arduino.h> | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.esp8266"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
|   this->pin_->setup(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_); | ||||
|   ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     uint32_t raw = 0; | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|     raw = ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance) | ||||
| #else | ||||
|     raw = analogRead(this->pin_->get_pin());  // NOLINT | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
|   if (this->output_raw_) { | ||||
|     return aggr.aggregate(); | ||||
|   } | ||||
|   return aggr.aggregate() / 1024.0f; | ||||
| } | ||||
|  | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP8266 | ||||
							
								
								
									
										53
									
								
								esphome/components/adc/adc_sensor_libretiny.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								esphome/components/adc/adc_sensor_libretiny.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| #ifdef USE_LIBRETINY | ||||
|  | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.libretiny"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
|   this->pin_->setup(); | ||||
| #endif  // !USE_ADC_SENSOR_VCC | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else   // USE_ADC_SENSOR_VCC | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_); | ||||
|   ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   uint32_t raw = 0; | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|   if (this->output_raw_) { | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       raw = analogRead(this->pin_->get_pin());  // NOLINT | ||||
|       aggr.add_sample(raw); | ||||
|     } | ||||
|     return aggr.aggregate(); | ||||
|   } | ||||
|  | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     raw = analogReadVoltage(this->pin_->get_pin());  // NOLINT | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
|   return aggr.aggregate() / 1000.0f; | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_LIBRETINY | ||||
							
								
								
									
										96
									
								
								esphome/components/adc/adc_sensor_rp2040.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								esphome/components/adc/adc_sensor_rp2040.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| #ifdef USE_RP2040 | ||||
|  | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
| #include "pico/cyw43_arch.h" | ||||
| #endif  // CYW43_USES_VSYS_PIN | ||||
| #include <hardware/adc.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.rp2040"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
|   static bool initialized = false; | ||||
|   if (!initialized) { | ||||
|     adc_init(); | ||||
|     initialized = true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   if (this->is_temperature_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Pin: Temperature"); | ||||
|   } else { | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|     ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else | ||||
|     LOG_PIN("  Pin: ", this->pin_); | ||||
| #endif  // USE_ADC_SENSOR_VCC | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_); | ||||
|   ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   uint32_t raw = 0; | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|   if (this->is_temperature_) { | ||||
|     adc_set_temp_sensor_enabled(true); | ||||
|     delay(1); | ||||
|     adc_select_input(4); | ||||
|  | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       raw = adc_read(); | ||||
|       aggr.add_sample(raw); | ||||
|     } | ||||
|     adc_set_temp_sensor_enabled(false); | ||||
|     if (this->output_raw_) { | ||||
|       return aggr.aggregate(); | ||||
|     } | ||||
|     return aggr.aggregate() * 3.3f / 4096.0f; | ||||
|   } | ||||
|  | ||||
|   uint8_t pin = this->pin_->get_pin(); | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
|   if (pin == PICO_VSYS_PIN) { | ||||
|     // Measuring VSYS on Raspberry Pico W needs to be wrapped with | ||||
|     // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in | ||||
|     // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and | ||||
|     // VSYS ADC both share GPIO29 | ||||
|     cyw43_thread_enter(); | ||||
|   } | ||||
| #endif  // CYW43_USES_VSYS_PIN | ||||
|  | ||||
|   adc_gpio_init(pin); | ||||
|   adc_select_input(pin - 26); | ||||
|  | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     raw = adc_read(); | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
| #ifdef CYW43_USES_VSYS_PIN | ||||
|   if (pin == PICO_VSYS_PIN) { | ||||
|     cyw43_thread_exit(); | ||||
|   } | ||||
| #endif  // CYW43_USES_VSYS_PIN | ||||
|  | ||||
|   if (this->output_raw_) { | ||||
|     return aggr.aggregate(); | ||||
|   } | ||||
|   float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f; | ||||
|   return aggr.aggregate() * 3.3f / 4096.0f * coeff; | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_RP2040 | ||||
| @@ -1,11 +1,9 @@ | ||||
| import logging | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
| from esphome.core import CORE | ||||
| from esphome.components import sensor, voltage_sampler | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
|     CONF_ID, | ||||
| @@ -17,10 +15,14 @@ from esphome.const import ( | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
|     ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, | ||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||
|     SAMPLING_MODES, | ||||
|     adc_ns, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
| @@ -30,9 +32,11 @@ _LOGGER = logging.getLogger(__name__) | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
|  | ||||
| CONF_SAMPLES = "samples" | ||||
| CONF_SAMPLING_MODE = "sampling_mode" | ||||
|  | ||||
|  | ||||
| _attenuation = cv.enum(ATTENUATION_MODES, lower=True) | ||||
| _sampling_mode = cv.enum(SAMPLING_MODES, lower=True) | ||||
|  | ||||
|  | ||||
| def validate_config(config): | ||||
| @@ -88,6 +92,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.only_on_esp32, _attenuation | ||||
|             ), | ||||
|             cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), | ||||
|             cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")), | ||||
| @@ -112,6 +117,7 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add(var.set_output_raw(config[CONF_RAW])) | ||||
|     cg.add(var.set_sample_count(config[CONF_SAMPLES])) | ||||
|     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||
|  | ||||
|     if attenuation := config.get(CONF_ATTENUATION): | ||||
|         if attenuation == "auto": | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The ade7953 sensor component has been renamed to ade7953_i2c." | ||||
| ) | ||||
|   | ||||
| @@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) { | ||||
|   this->write_byte16(reg); | ||||
|   this->transfer_byte(0x80); | ||||
|   uint8_t recv[2]; | ||||
|   this->read_array(recv, 4); | ||||
|   this->read_array(recv, 2); | ||||
|   *value = encode_uint16(recv[0], recv[1]); | ||||
|   this->disable(); | ||||
|   return false; | ||||
|   | ||||
| @@ -9,8 +9,6 @@ static const char *const TAG = "ads1115"; | ||||
| static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; | ||||
| static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; | ||||
|  | ||||
| static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;  // 3300_SPS for ADS1015 | ||||
|  | ||||
| void ADS1115Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); | ||||
|   uint16_t value; | ||||
| @@ -43,9 +41,9 @@ void ADS1115Component::setup() { | ||||
|     config |= 0b0000000100000000; | ||||
|   } | ||||
|  | ||||
|   // Set data rate - 860 samples per second (we're in singleshot mode) | ||||
|   // Set data rate - 860 samples per second | ||||
|   //        0bxxxxxxxx100xxxxx | ||||
|   config |= ADS1115_DATA_RATE_860_SPS << 5; | ||||
|   config |= ADS1115_860SPS << 5; | ||||
|  | ||||
|   // Set comparator mode - hysteresis | ||||
|   //        0bxxxxxxxxxxx0xxxx | ||||
| @@ -77,7 +75,7 @@ void ADS1115Component::dump_config() { | ||||
|   } | ||||
| } | ||||
| float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, | ||||
|                                             ADS1115Resolution resolution) { | ||||
|                                             ADS1115Resolution resolution, ADS1115Samplerate samplerate) { | ||||
|   uint16_t config = this->prev_config_; | ||||
|   // Multiplexer | ||||
|   //        0bxBBBxxxxxxxxxxxx | ||||
| @@ -89,6 +87,11 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 | ||||
|   config &= 0b1111000111111111; | ||||
|   config |= (gain & 0b111) << 9; | ||||
|  | ||||
|   // Sample rate | ||||
|   //        0bxxxxxxxxBBBxxxxx | ||||
|   config &= 0b1111111100011111; | ||||
|   config |= (samplerate & 0b111) << 5; | ||||
|  | ||||
|   if (!this->continuous_mode_) { | ||||
|     // Start conversion | ||||
|     config |= 0b1000000000000000; | ||||
| @@ -101,8 +104,54 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 | ||||
|     } | ||||
|     this->prev_config_ = config; | ||||
|  | ||||
|     // about 1.2 ms with 860 samples per second | ||||
|     delay(2); | ||||
|     // Delay calculated as: ceil((1000/SPS)+.5) | ||||
|     if (resolution == ADS1015_12_BITS) { | ||||
|       switch (samplerate) { | ||||
|         case ADS1115_8SPS: | ||||
|           delay(9); | ||||
|           break; | ||||
|         case ADS1115_16SPS: | ||||
|           delay(5); | ||||
|           break; | ||||
|         case ADS1115_32SPS: | ||||
|           delay(3); | ||||
|           break; | ||||
|         case ADS1115_64SPS: | ||||
|         case ADS1115_128SPS: | ||||
|           delay(2); | ||||
|           break; | ||||
|         default: | ||||
|           delay(1); | ||||
|           break; | ||||
|       } | ||||
|     } else { | ||||
|       switch (samplerate) { | ||||
|         case ADS1115_8SPS: | ||||
|           delay(126);  // NOLINT | ||||
|           break; | ||||
|         case ADS1115_16SPS: | ||||
|           delay(63);  // NOLINT | ||||
|           break; | ||||
|         case ADS1115_32SPS: | ||||
|           delay(32); | ||||
|           break; | ||||
|         case ADS1115_64SPS: | ||||
|           delay(17); | ||||
|           break; | ||||
|         case ADS1115_128SPS: | ||||
|           delay(9); | ||||
|           break; | ||||
|         case ADS1115_250SPS: | ||||
|           delay(5); | ||||
|           break; | ||||
|         case ADS1115_475SPS: | ||||
|           delay(3); | ||||
|           break; | ||||
|         case ADS1115_860SPS: | ||||
|           delay(2); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // in continuous mode, conversion will always be running, rely on the delay | ||||
|     // to ensure conversion is taking place with the correct settings | ||||
|   | ||||
| @@ -33,6 +33,17 @@ enum ADS1115Resolution { | ||||
|   ADS1015_12_BITS = 12, | ||||
| }; | ||||
|  | ||||
| enum ADS1115Samplerate { | ||||
|   ADS1115_8SPS = 0b000, | ||||
|   ADS1115_16SPS = 0b001, | ||||
|   ADS1115_32SPS = 0b010, | ||||
|   ADS1115_64SPS = 0b011, | ||||
|   ADS1115_128SPS = 0b100, | ||||
|   ADS1115_250SPS = 0b101, | ||||
|   ADS1115_475SPS = 0b110, | ||||
|   ADS1115_860SPS = 0b111 | ||||
| }; | ||||
|  | ||||
| class ADS1115Component : public Component, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
| @@ -42,7 +53,8 @@ class ADS1115Component : public Component, public i2c::I2CDevice { | ||||
|   void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } | ||||
|  | ||||
|   /// Helper method to request a measurement from a sensor. | ||||
|   float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution); | ||||
|   float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, | ||||
|                             ADS1115Samplerate samplerate); | ||||
|  | ||||
|  protected: | ||||
|   uint16_t prev_config_{0}; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from esphome.const import ( | ||||
|     CONF_GAIN, | ||||
|     CONF_MULTIPLEXER, | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_SAMPLE_RATE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| @@ -43,6 +44,17 @@ RESOLUTION = { | ||||
|     "12_BITS": ADS1115Resolution.ADS1015_12_BITS, | ||||
| } | ||||
|  | ||||
| ADS1115Samplerate = ads1115_ns.enum("ADS1115Samplerate") | ||||
| SAMPLERATE = { | ||||
|     "8": ADS1115Samplerate.ADS1115_8SPS, | ||||
|     "16": ADS1115Samplerate.ADS1115_16SPS, | ||||
|     "32": ADS1115Samplerate.ADS1115_32SPS, | ||||
|     "64": ADS1115Samplerate.ADS1115_64SPS, | ||||
|     "128": ADS1115Samplerate.ADS1115_128SPS, | ||||
|     "250": ADS1115Samplerate.ADS1115_250SPS, | ||||
|     "475": ADS1115Samplerate.ADS1115_475SPS, | ||||
|     "860": ADS1115Samplerate.ADS1115_860SPS, | ||||
| } | ||||
|  | ||||
| ADS1115Sensor = ads1115_ns.class_( | ||||
|     "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||
| @@ -64,6 +76,9 @@ CONFIG_SCHEMA = ( | ||||
|             cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( | ||||
|                 RESOLUTION, upper=True, space="_" | ||||
|             ), | ||||
|             cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum( | ||||
|                 SAMPLERATE, string=True | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| @@ -79,3 +94,4 @@ async def to_code(config): | ||||
|     cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) | ||||
|     cg.add(var.set_gain(config[CONF_GAIN])) | ||||
|     cg.add(var.set_resolution(config[CONF_RESOLUTION])) | ||||
|     cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE])) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace ads1115 { | ||||
| static const char *const TAG = "ads1115.sensor"; | ||||
|  | ||||
| float ADS1115Sensor::sample() { | ||||
|   return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_); | ||||
|   return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); | ||||
| } | ||||
|  | ||||
| void ADS1115Sensor::update() { | ||||
| @@ -24,6 +24,7 @@ void ADS1115Sensor::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "    Multiplexer: %u", this->multiplexer_); | ||||
|   ESP_LOGCONFIG(TAG, "    Gain: %u", this->gain_); | ||||
|   ESP_LOGCONFIG(TAG, "    Resolution: %u", this->resolution_); | ||||
|   ESP_LOGCONFIG(TAG, "    Sample rate: %u", this->samplerate_); | ||||
| } | ||||
|  | ||||
| }  // namespace ads1115 | ||||
|   | ||||
| @@ -21,6 +21,7 @@ class ADS1115Sensor : public sensor::Sensor, | ||||
|   void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } | ||||
|   void set_gain(ADS1115Gain gain) { this->gain_ = gain; } | ||||
|   void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } | ||||
|   void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; } | ||||
|   float sample() override; | ||||
|  | ||||
|   void dump_config() override; | ||||
| @@ -29,6 +30,7 @@ class ADS1115Sensor : public sensor::Sensor, | ||||
|   ADS1115Multiplexer multiplexer_; | ||||
|   ADS1115Gain gain_; | ||||
|   ADS1115Resolution resolution_; | ||||
|   ADS1115Samplerate samplerate_; | ||||
| }; | ||||
|  | ||||
| }  // namespace ads1115 | ||||
|   | ||||
| @@ -93,8 +93,9 @@ void AHT10Component::restart_read_() { | ||||
|  | ||||
| void AHT10Component::read_data_() { | ||||
|   uint8_t data[6]; | ||||
|   if (this->read_count_ > 1) | ||||
|   if (this->read_count_ > 1) { | ||||
|     ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); | ||||
|   } | ||||
|   if (this->read(data, 6) != i2c::ERROR_OK) { | ||||
|     this->status_set_warning("AHT10 read failed, retrying soon"); | ||||
|     this->restart_read_(); | ||||
| @@ -119,8 +120,9 @@ void AHT10Component::read_data_() { | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   if (this->read_count_ > 1) | ||||
|   if (this->read_count_ > 1) { | ||||
|     ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_)); | ||||
|   } | ||||
|   uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; | ||||
|   uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; | ||||
|  | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/aic3204/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/aic3204/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										173
									
								
								esphome/components/aic3204/aic3204.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								esphome/components/aic3204/aic3204.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| #include "aic3204.h" | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace aic3204 { | ||||
|  | ||||
| static const char *const TAG = "aic3204"; | ||||
|  | ||||
| #define ERROR_CHECK(err, msg) \ | ||||
|   if (!(err)) { \ | ||||
|     ESP_LOGE(TAG, msg); \ | ||||
|     this->mark_failed(); \ | ||||
|     return; \ | ||||
|   } | ||||
|  | ||||
| void AIC3204::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up AIC3204..."); | ||||
|  | ||||
|   // Set register page to 0 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); | ||||
|   // Initiate SW reset (PLL is powered off as part of reset) | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed"); | ||||
|   // *** Program clock settings *** | ||||
|   // Default is CODEC_CLKIN is from MCLK pin. Don't need to change this. | ||||
|   // MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio) | ||||
|   // (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf) | ||||
|   // We do need MDAC*DOSR/32 >= the resource compute level for the processing block | ||||
|   // So here 2*128/32 = 8, which is equal to processing block 1 's resource compute | ||||
|   // See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow | ||||
|   // for determining these settings. | ||||
|  | ||||
|   // Power up NDAC and set to 2 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed"); | ||||
|   // Power up MDAC and set to 2 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed"); | ||||
|   // Program DOSR = 128 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed"); | ||||
|   // Set Audio Interface Config: I2S, 32 bits, DOUT always driving | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed"); | ||||
|   // For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed"); | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed"); | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed"); | ||||
|   // Program the DAC processing block to be used - PRB_P1 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed"); | ||||
|  | ||||
|   // *** Select Page 1 *** | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed"); | ||||
|   // Enable the internal AVDD_LDO: | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed"); | ||||
|   // *** Program Analog Blocks *** | ||||
|   // Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed"); | ||||
|   // Enable Master Analog Power Control | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed"); | ||||
|   // Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v | ||||
|   // We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123) | ||||
|   // Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware | ||||
|   // We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47) | ||||
|   // (All pages refer to the TLV320AIC3204 Application Reference Guide) | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed"); | ||||
|   // *** Set PowerTune Modes *** | ||||
|   // Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver. | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed"); | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed"); | ||||
|   // Set the REF charging time to 40ms | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed"); | ||||
|   // HP soft stepping settings for optimal pop performance at power up | ||||
|   // Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling | ||||
|   // capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound. | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed"); | ||||
|   // Route Left DAC to HPL | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed"); | ||||
|   // Route Right DAC to HPR | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed"); | ||||
|   // Route Left DAC to LOL | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed"); | ||||
|   // Route Right DAC to LOR | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed"); | ||||
|  | ||||
|   // Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed"); | ||||
|   // Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register) | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed"); | ||||
|   // Unmute LOL and set gain to 0dB | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed"); | ||||
|   // Unmute LOR and set gain to 0dB | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed"); | ||||
|  | ||||
|   // Power up HPL and HPR, LOL and LOR drivers | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed"); | ||||
|  | ||||
|   // Wait for 2.5 sec for soft stepping to take effect before attempting power-up | ||||
|   this->set_timeout(2500, [this]() { | ||||
|     // *** Power Up DAC *** | ||||
|     // Select Page 0 | ||||
|     ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set PAGE_CTRL failed"); | ||||
|     // Power up the Left and Right DAC Channels. Route Left data to Left DAC and Right data to Right DAC. | ||||
|     // DAC Vol control soft step 1 step per DAC word clock. | ||||
|     ERROR_CHECK(this->write_byte(AIC3204_DAC_CH_SET1, 0xd4), "Set DAC_CH_SET1 failed"); | ||||
|     // Set left and right DAC digital volume control | ||||
|     ERROR_CHECK(this->write_volume_(), "Set volume failed"); | ||||
|     // Unmute left and right channels | ||||
|     ERROR_CHECK(this->write_mute_(), "Set mute failed"); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void AIC3204::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "AIC3204:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with AIC3204 failed"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool AIC3204::set_mute_off() { | ||||
|   this->is_muted_ = false; | ||||
|   return this->write_mute_(); | ||||
| } | ||||
|  | ||||
| bool AIC3204::set_mute_on() { | ||||
|   this->is_muted_ = true; | ||||
|   return this->write_mute_(); | ||||
| } | ||||
|  | ||||
| bool AIC3204::set_auto_mute_mode(uint8_t auto_mute_mode) { | ||||
|   this->auto_mute_mode_ = auto_mute_mode & 0x07; | ||||
|   ESP_LOGVV(TAG, "Setting auto_mute_mode to 0x%.2x", this->auto_mute_mode_); | ||||
|   return this->write_mute_(); | ||||
| } | ||||
|  | ||||
| bool AIC3204::set_volume(float volume) { | ||||
|   this->volume_ = clamp<float>(volume, 0.0, 1.0); | ||||
|   return this->write_volume_(); | ||||
| } | ||||
|  | ||||
| bool AIC3204::is_muted() { return this->is_muted_; } | ||||
|  | ||||
| float AIC3204::volume() { return this->volume_; } | ||||
|  | ||||
| bool AIC3204::write_mute_() { | ||||
|   uint8_t mute_mode_byte = this->auto_mute_mode_ << 4;  // auto-mute control is bits 4-6 | ||||
|   mute_mode_byte |= this->is_muted_ ? 0x0c : 0x00;      // mute bits are 2-3 | ||||
|   if (!this->write_byte(AIC3204_PAGE_CTRL, 0x00) || !this->write_byte(AIC3204_DAC_CH_SET2, mute_mode_byte)) { | ||||
|     ESP_LOGE(TAG, "Writing mute modes failed"); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool AIC3204::write_volume_() { | ||||
|   const int8_t dvc_min_byte = -127; | ||||
|   const int8_t dvc_max_byte = 48; | ||||
|  | ||||
|   int8_t volume_byte = dvc_min_byte + (this->volume_ * (dvc_max_byte - dvc_min_byte)); | ||||
|   volume_byte = clamp<int8_t>(volume_byte, dvc_min_byte, dvc_max_byte); | ||||
|  | ||||
|   ESP_LOGVV(TAG, "Setting volume to 0x%.2x", volume_byte & 0xFF); | ||||
|  | ||||
|   if ((!this->write_byte(AIC3204_PAGE_CTRL, 0x00)) || (!this->write_byte(AIC3204_DACL_VOL_D, volume_byte)) || | ||||
|       (!this->write_byte(AIC3204_DACR_VOL_D, volume_byte))) { | ||||
|     ESP_LOGE(TAG, "Writing volume failed"); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| }  // namespace aic3204 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										88
									
								
								esphome/components/aic3204/aic3204.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/aic3204/aic3204.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/audio_dac/audio_dac.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace aic3204 { | ||||
|  | ||||
| // TLV320AIC3204 Register Addresses | ||||
| // Page 0 | ||||
| static const uint8_t AIC3204_PAGE_CTRL = 0x00;     // Register 0  - Page Control | ||||
| static const uint8_t AIC3204_SW_RST = 0x01;        // Register 1  - Software Reset | ||||
| static const uint8_t AIC3204_CLK_PLL1 = 0x04;      // Register 4  - Clock Setting Register 1, Multiplexers | ||||
| static const uint8_t AIC3204_CLK_PLL2 = 0x05;      // Register 5  - Clock Setting Register 2, P and R values | ||||
| static const uint8_t AIC3204_CLK_PLL3 = 0x06;      // Register 6  - Clock Setting Register 3, J values | ||||
| static const uint8_t AIC3204_NDAC = 0x0B;          // Register 11 - NDAC Divider Value | ||||
| static const uint8_t AIC3204_MDAC = 0x0C;          // Register 12 - MDAC Divider Value | ||||
| static const uint8_t AIC3204_DOSR = 0x0E;          // Register 14 - DOSR Divider Value (LS Byte) | ||||
| static const uint8_t AIC3204_NADC = 0x12;          // Register 18 - NADC Divider Value | ||||
| static const uint8_t AIC3204_MADC = 0x13;          // Register 19 - MADC Divider Value | ||||
| static const uint8_t AIC3204_AOSR = 0x14;          // Register 20 - AOSR Divider Value | ||||
| static const uint8_t AIC3204_CODEC_IF = 0x1B;      // Register 27 - CODEC Interface Control | ||||
| static const uint8_t AIC3204_AUDIO_IF_4 = 0x1F;    // Register 31 - Audio Interface Setting Register 4 | ||||
| static const uint8_t AIC3204_AUDIO_IF_5 = 0x20;    // Register 32 - Audio Interface Setting Register 5 | ||||
| static const uint8_t AIC3204_SCLK_MFP3 = 0x38;     // Register 56 - SCLK/MFP3 Function Control | ||||
| static const uint8_t AIC3204_DAC_SIG_PROC = 0x3C;  // Register 60 - DAC Sig Processing Block Control | ||||
| static const uint8_t AIC3204_ADC_SIG_PROC = 0x3D;  // Register 61 - ADC Sig Processing Block Control | ||||
| static const uint8_t AIC3204_DAC_CH_SET1 = 0x3F;   // Register 63 - DAC Channel Setup 1 | ||||
| static const uint8_t AIC3204_DAC_CH_SET2 = 0x40;   // Register 64 - DAC Channel Setup 2 | ||||
| static const uint8_t AIC3204_DACL_VOL_D = 0x41;    // Register 65 - DAC Left Digital Vol Control | ||||
| static const uint8_t AIC3204_DACR_VOL_D = 0x42;    // Register 66 - DAC Right Digital Vol Control | ||||
| static const uint8_t AIC3204_DRC_ENABLE = 0x44; | ||||
| static const uint8_t AIC3204_ADC_CH_SET = 0x51;    // Register 81 - ADC Channel Setup | ||||
| static const uint8_t AIC3204_ADC_FGA_MUTE = 0x52;  // Register 82 - ADC Fine Gain Adjust/Mute | ||||
|  | ||||
| // Page 1 | ||||
| static const uint8_t AIC3204_PWR_CFG = 0x01;       // Register 1  - Power Config | ||||
| static const uint8_t AIC3204_LDO_CTRL = 0x02;      // Register 2  - LDO Control | ||||
| static const uint8_t AIC3204_PLAY_CFG1 = 0x03;     // Register 3  - Playback Config 1 | ||||
| static const uint8_t AIC3204_PLAY_CFG2 = 0x04;     // Register 4  - Playback Config 2 | ||||
| static const uint8_t AIC3204_OP_PWR_CTRL = 0x09;   // Register 9  - Output Driver Power Control | ||||
| static const uint8_t AIC3204_CM_CTRL = 0x0A;       // Register 10 - Common Mode Control | ||||
| static const uint8_t AIC3204_HPL_ROUTE = 0x0C;     // Register 12 - HPL Routing Select | ||||
| static const uint8_t AIC3204_HPR_ROUTE = 0x0D;     // Register 13 - HPR Routing Select | ||||
| static const uint8_t AIC3204_LOL_ROUTE = 0x0E;     // Register 14 - LOL Routing Selection | ||||
| static const uint8_t AIC3204_LOR_ROUTE = 0x0F;     // Register 15 - LOR Routing Selection | ||||
| static const uint8_t AIC3204_HPL_GAIN = 0x10;      // Register 16 - HPL Driver Gain | ||||
| static const uint8_t AIC3204_HPR_GAIN = 0x11;      // Register 17 - HPR Driver Gain | ||||
| static const uint8_t AIC3204_LOL_DRV_GAIN = 0x12;  // Register 18 - LOL Driver Gain Setting | ||||
| static const uint8_t AIC3204_LOR_DRV_GAIN = 0x13;  // Register 19 - LOR Driver Gain Setting | ||||
| static const uint8_t AIC3204_HP_START = 0x14;      // Register 20 - Headphone Driver Startup | ||||
| static const uint8_t AIC3204_LPGA_P_ROUTE = 0x34;  // Register 52 - Left PGA Positive Input Route | ||||
| static const uint8_t AIC3204_LPGA_N_ROUTE = 0x36;  // Register 54 - Left PGA Negative Input Route | ||||
| static const uint8_t AIC3204_RPGA_P_ROUTE = 0x37;  // Register 55 - Right PGA Positive Input Route | ||||
| static const uint8_t AIC3204_RPGA_N_ROUTE = 0x39;  // Register 57 - Right PGA Negative Input Route | ||||
| static const uint8_t AIC3204_LPGA_VOL = 0x3B;      // Register 59 - Left PGA Volume | ||||
| static const uint8_t AIC3204_RPGA_VOL = 0x3C;      // Register 60 - Right PGA Volume | ||||
| static const uint8_t AIC3204_ADC_PTM = 0x3D;       // Register 61 - ADC Power Tune Config | ||||
| static const uint8_t AIC3204_AN_IN_CHRG = 0x47;    // Register 71 - Analog Input Quick Charging Config | ||||
| static const uint8_t AIC3204_REF_STARTUP = 0x7B;   // Register 123 - Reference Power Up Config | ||||
|  | ||||
| class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|   bool set_mute_off() override; | ||||
|   bool set_mute_on() override; | ||||
|   bool set_auto_mute_mode(uint8_t auto_mute_mode); | ||||
|   bool set_volume(float volume) override; | ||||
|  | ||||
|   bool is_muted() override; | ||||
|   float volume() override; | ||||
|  | ||||
|  protected: | ||||
|   bool write_mute_(); | ||||
|   bool write_volume_(); | ||||
|  | ||||
|   uint8_t auto_mute_mode_{0}; | ||||
|   float volume_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace aic3204 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										52
									
								
								esphome/components/aic3204/audio_dac.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/aic3204/audio_dac.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c | ||||
| from esphome.components.audio_dac import AudioDac | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_MODE | ||||
|  | ||||
| CODEOWNERS = ["@kbx81"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| aic3204_ns = cg.esphome_ns.namespace("aic3204") | ||||
| AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice) | ||||
|  | ||||
| SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AIC3204), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(i2c.i2c_device_schema(0x18)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(AIC3204), | ||||
|         cv.Required(CONF_MODE): cv.templatable(cv.int_range(max=7, min=0)), | ||||
|     }, | ||||
|     key=CONF_MODE, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA | ||||
| ) | ||||
| async def aic3204_set_volume_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|  | ||||
|     template_ = await cg.templatable(config.get(CONF_MODE), args, int) | ||||
|     cg.add(var.set_auto_mute_mode(template_)) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
							
								
								
									
										23
									
								
								esphome/components/aic3204/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/aic3204/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "aic3204.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace aic3204 { | ||||
|  | ||||
| template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> { | ||||
|  public: | ||||
|   explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {} | ||||
|  | ||||
|   TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) | ||||
|  | ||||
|   void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } | ||||
|  | ||||
|  protected: | ||||
|   AIC3204 *aic3204_; | ||||
| }; | ||||
|  | ||||
| }  // namespace aic3204 | ||||
| }  // namespace esphome | ||||
| @@ -14,8 +14,6 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||
|     ESP_LOGD(TAG, "version = %d", value->version); | ||||
|  | ||||
|     if (value->version == 1) { | ||||
|       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||
|  | ||||
|       if (this->humidity_sensor_ != nullptr) { | ||||
|         this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||
|       } | ||||
| @@ -43,6 +41,10 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||
|       if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { | ||||
|         this->tvoc_sensor_->publish_state(value->voc); | ||||
|       } | ||||
|  | ||||
|       if (this->illuminance_sensor_ != nullptr) { | ||||
|         this->illuminance_sensor_->publish_state(value->ambientLight); | ||||
|       } | ||||
|     } else { | ||||
|       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); | ||||
|     } | ||||
| @@ -68,6 +70,7 @@ void AirthingsWavePlus::dump_config() { | ||||
|   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||
|   LOG_SENSOR("  ", "Illuminance", this->illuminance_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWavePlus::AirthingsWavePlus() { | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||
|   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } | ||||
|   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||
|   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||
|   void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_radon_value_(uint16_t radon); | ||||
| @@ -32,6 +33,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||
|   sensor::Sensor *radon_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||
|   sensor::Sensor *co2_sensor_{nullptr}; | ||||
|   sensor::Sensor *illuminance_sensor_{nullptr}; | ||||
|  | ||||
|   struct WavePlusReadings { | ||||
|     uint8_t version; | ||||
|   | ||||
| @@ -12,6 +12,9 @@ from esphome.const import ( | ||||
|     CONF_CO2, | ||||
|     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|     UNIT_PARTS_PER_MILLION, | ||||
|     CONF_ILLUMINANCE, | ||||
|     UNIT_LUX, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||
| @@ -45,6 +48,12 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||
|             device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_LUX, | ||||
|             accuracy_decimals=0, | ||||
|             device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -62,3 +71,6 @@ async def to_code(config): | ||||
|     if config_co2 := config.get(CONF_CO2): | ||||
|         sens = await sensor.new_sensor(config_co2) | ||||
|         cg.add(var.set_co2(sens)) | ||||
|     if config_illuminance := config.get(CONF_ILLUMINANCE): | ||||
|         sens = await sensor.new_sensor(config_illuminance) | ||||
|         cg.add(var.set_illuminance(sens)) | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import web_server | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import mqtt, web_server | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CODE, | ||||
|     CONF_ID, | ||||
|     CONF_MQTT_ID, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_CODE, | ||||
|     CONF_WEB_SERVER_ID, | ||||
|     CONF_WEB_SERVER, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@grahambrown11", "@hwstar"] | ||||
| @@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_( | ||||
|     "AlarmControlPanelCondition", automation.Condition | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     web_server.WEBSERVER_SORTING_SCHEMA | ||||
| ).extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||
|         cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMING): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_PENDING): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_DISARMED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CHIME): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_READY): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ALARM_CONTROL_PANEL_SCHEMA = ( | ||||
|     cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) | ||||
|     .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||
|             cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( | ||||
|                 mqtt.MQTTAlarmControlPanelComponent | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMING): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PENDING): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_DISARMED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CHIME): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_READY): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||
| @@ -189,9 +195,11 @@ async def setup_alarm_control_panel_core_(var, config): | ||||
|     for conf in config.get(CONF_ON_READY, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None: | ||||
|         web_server_ = await cg.get_variable(webserver_id) | ||||
|         web_server.add_entity_to_sorting_list(web_server_, var, config) | ||||
|     if web_server_config := config.get(CONF_WEB_SERVER): | ||||
|         await web_server.add_entity_config(var, web_server_config) | ||||
|     if mqtt_id := config.get(CONF_MQTT_ID): | ||||
|         mqtt_ = cg.new_Pvariable(mqtt_id, var) | ||||
|         await mqtt.register_mqtt_component(mqtt_, config) | ||||
|  | ||||
|  | ||||
| async def register_alarm_control_panel(var, config): | ||||
|   | ||||
| @@ -72,10 +72,9 @@ void AlarmControlPanelCall::validate_() { | ||||
|       this->state_.reset(); | ||||
|       return; | ||||
|     } | ||||
|     if (state == ACP_STATE_DISARMED && | ||||
|         !(this->parent_->is_state_armed(this->parent_->get_state()) || | ||||
|           this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING || | ||||
|           this->parent_->get_state() == ACP_STATE_TRIGGERED)) { | ||||
|     if (state == ACP_STATE_DISARMED && !this->parent_->is_state_armed(this->parent_->get_state()) && | ||||
|         this->parent_->get_state() != ACP_STATE_PENDING && this->parent_->get_state() != ACP_STATE_ARMING && | ||||
|         this->parent_->get_state() != ACP_STATE_TRIGGERED) { | ||||
|       ESP_LOGW(TAG, "Cannot disarm when not armed"); | ||||
|       this->state_.reset(); | ||||
|       return; | ||||
|   | ||||
| @@ -1,29 +1,10 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import automation, core | ||||
| from esphome.components import font | ||||
| import esphome.components.image as espImage | ||||
| from esphome.components.image import ( | ||||
|     CONF_USE_TRANSPARENCY, | ||||
|     LOCAL_SCHEMA, | ||||
|     WEB_SCHEMA, | ||||
|     SOURCE_WEB, | ||||
|     SOURCE_LOCAL, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import ( | ||||
|     CONF_FILE, | ||||
|     CONF_ID, | ||||
|     CONF_RAW_DATA_ID, | ||||
|     CONF_REPEAT, | ||||
|     CONF_RESIZE, | ||||
|     CONF_TYPE, | ||||
|     CONF_SOURCE, | ||||
|     CONF_PATH, | ||||
|     CONF_URL, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt | ||||
| import esphome.components.image as espImage | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_REPEAT | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -31,6 +12,7 @@ AUTO_LOAD = ["image"] | ||||
| CODEOWNERS = ["@syndlex"] | ||||
| DEPENDENCIES = ["display"] | ||||
| MULTI_CONF = True | ||||
| MULTI_CONF_NO_DEFAULT = True | ||||
|  | ||||
| CONF_LOOP = "loop" | ||||
| CONF_START_FRAME = "start_frame" | ||||
| @@ -52,86 +34,19 @@ SetFrameAction = animation_ns.class_( | ||||
|     "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) | ||||
| ) | ||||
|  | ||||
| TYPED_FILE_SCHEMA = cv.typed_schema( | ||||
| CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( | ||||
|     { | ||||
|         SOURCE_LOCAL: LOCAL_SCHEMA, | ||||
|         SOURCE_WEB: WEB_SCHEMA, | ||||
|     }, | ||||
|     key=CONF_SOURCE, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _file_schema(value): | ||||
|     if isinstance(value, str): | ||||
|         return validate_file_shorthand(value) | ||||
|     return TYPED_FILE_SCHEMA(value) | ||||
|  | ||||
|  | ||||
| FILE_SCHEMA = cv.Schema(_file_schema) | ||||
|  | ||||
|  | ||||
| def validate_file_shorthand(value): | ||||
|     value = cv.string_strict(value) | ||||
|     if value.startswith("http://") or value.startswith("https://"): | ||||
|         return FILE_SCHEMA( | ||||
|         cv.Required(CONF_ID): cv.declare_id(Animation_), | ||||
|         cv.Optional(CONF_LOOP): cv.All( | ||||
|             { | ||||
|                 CONF_SOURCE: SOURCE_WEB, | ||||
|                 CONF_URL: value, | ||||
|                 cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, | ||||
|                 cv.Optional(CONF_END_FRAME): cv.positive_int, | ||||
|                 cv.Optional(CONF_REPEAT): cv.positive_int, | ||||
|             } | ||||
|         ) | ||||
|     return FILE_SCHEMA( | ||||
|         { | ||||
|             CONF_SOURCE: SOURCE_LOCAL, | ||||
|             CONF_PATH: value, | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def validate_cross_dependencies(config): | ||||
|     """ | ||||
|     Validate fields whose possible values depend on other fields. | ||||
|     For example, validate that explicitly transparent image types | ||||
|     have "use_transparency" set to True. | ||||
|     Also set the default value for those kind of dependent fields. | ||||
|     """ | ||||
|     image_type = config[CONF_TYPE] | ||||
|     is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] | ||||
|     # If the use_transparency option was not specified, set the default depending on the image type | ||||
|     if CONF_USE_TRANSPARENCY not in config: | ||||
|         config[CONF_USE_TRANSPARENCY] = is_transparent_type | ||||
|  | ||||
|     if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: | ||||
|         raise cv.Invalid(f"Image type {image_type} must always be transparent.") | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| ANIMATION_SCHEMA = cv.Schema( | ||||
|     cv.All( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.declare_id(Animation_), | ||||
|             cv.Required(CONF_FILE): FILE_SCHEMA, | ||||
|             cv.Optional(CONF_RESIZE): cv.dimensions, | ||||
|             cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( | ||||
|                 espImage.IMAGE_TYPE, upper=True | ||||
|             ), | ||||
|             # Not setting default here on purpose; the default depends on the image type, | ||||
|             # and thus will be set in the "validate_cross_dependencies" validator. | ||||
|             cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, | ||||
|             cv.Optional(CONF_LOOP): cv.All( | ||||
|                 { | ||||
|                     cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, | ||||
|                     cv.Optional(CONF_END_FRAME): cv.positive_int, | ||||
|                     cv.Optional(CONF_REPEAT): cv.positive_int, | ||||
|                 } | ||||
|             ), | ||||
|             cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), | ||||
|         }, | ||||
|         validate_cross_dependencies, | ||||
|     ) | ||||
|         ), | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) | ||||
|  | ||||
| NEXT_FRAME_SCHEMA = automation.maybe_simple_id( | ||||
|     { | ||||
| @@ -165,179 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args): | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     from PIL import Image | ||||
|     ( | ||||
|         prog_arr, | ||||
|         width, | ||||
|         height, | ||||
|         image_type, | ||||
|         trans_value, | ||||
|         frame_count, | ||||
|     ) = await espImage.write_image(config, all_frames=True) | ||||
|  | ||||
|     conf_file = config[CONF_FILE] | ||||
|     if conf_file[CONF_SOURCE] == SOURCE_LOCAL: | ||||
|         path = CORE.relative_config_path(conf_file[CONF_PATH]) | ||||
|     elif conf_file[CONF_SOURCE] == SOURCE_WEB: | ||||
|         path = espImage.compute_local_image_path(conf_file).as_posix() | ||||
|     try: | ||||
|         image = Image.open(path) | ||||
|     except Exception as e: | ||||
|         raise core.EsphomeError(f"Could not load image file {path}: {e}") | ||||
|  | ||||
|     width, height = image.size | ||||
|     frames = image.n_frames | ||||
|     if CONF_RESIZE in config: | ||||
|         new_width_max, new_height_max = config[CONF_RESIZE] | ||||
|         ratio = min(new_width_max / width, new_height_max / height) | ||||
|         width, height = int(width * ratio), int(height * ratio) | ||||
|     else: | ||||
|         if width > 500 or height > 500: | ||||
|             _LOGGER.warning( | ||||
|                 'The image "%s" you requested is very big. Please consider' | ||||
|                 " using the resize parameter.", | ||||
|                 path, | ||||
|             ) | ||||
|  | ||||
|     transparent = config[CONF_USE_TRANSPARENCY] | ||||
|  | ||||
|     if config[CONF_TYPE] == "GRAYSCALE": | ||||
|         data = [0 for _ in range(height * width * frames)] | ||||
|         pos = 0 | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             frame = image.convert("LA", dither=Image.Dither.NONE) | ||||
|             if CONF_RESIZE in config: | ||||
|                 frame = frame.resize([width, height]) | ||||
|             pixels = list(frame.getdata()) | ||||
|             if len(pixels) != height * width: | ||||
|                 raise core.EsphomeError( | ||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" | ||||
|                 ) | ||||
|             for pix, a in pixels: | ||||
|                 if transparent: | ||||
|                     if pix == 1: | ||||
|                         pix = 0 | ||||
|                     if a < 0x80: | ||||
|                         pix = 1 | ||||
|  | ||||
|                 data[pos] = pix | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] == "RGBA": | ||||
|         data = [0 for _ in range(height * width * 4 * frames)] | ||||
|         pos = 0 | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             frame = image.convert("RGBA") | ||||
|             if CONF_RESIZE in config: | ||||
|                 frame = frame.resize([width, height]) | ||||
|             pixels = list(frame.getdata()) | ||||
|             if len(pixels) != height * width: | ||||
|                 raise core.EsphomeError( | ||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" | ||||
|                 ) | ||||
|             for pix in pixels: | ||||
|                 data[pos] = pix[0] | ||||
|                 pos += 1 | ||||
|                 data[pos] = pix[1] | ||||
|                 pos += 1 | ||||
|                 data[pos] = pix[2] | ||||
|                 pos += 1 | ||||
|                 data[pos] = pix[3] | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] == "RGB24": | ||||
|         data = [0 for _ in range(height * width * 3 * frames)] | ||||
|         pos = 0 | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             frame = image.convert("RGBA") | ||||
|             if CONF_RESIZE in config: | ||||
|                 frame = frame.resize([width, height]) | ||||
|             pixels = list(frame.getdata()) | ||||
|             if len(pixels) != height * width: | ||||
|                 raise core.EsphomeError( | ||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" | ||||
|                 ) | ||||
|             for r, g, b, a in pixels: | ||||
|                 if transparent: | ||||
|                     if r == 0 and g == 0 and b == 1: | ||||
|                         b = 0 | ||||
|                     if a < 0x80: | ||||
|                         r = 0 | ||||
|                         g = 0 | ||||
|                         b = 1 | ||||
|  | ||||
|                 data[pos] = r | ||||
|                 pos += 1 | ||||
|                 data[pos] = g | ||||
|                 pos += 1 | ||||
|                 data[pos] = b | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: | ||||
|         data = [0 for _ in range(height * width * 2 * frames)] | ||||
|         pos = 0 | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             frame = image.convert("RGBA") | ||||
|             if CONF_RESIZE in config: | ||||
|                 frame = frame.resize([width, height]) | ||||
|             pixels = list(frame.getdata()) | ||||
|             if len(pixels) != height * width: | ||||
|                 raise core.EsphomeError( | ||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" | ||||
|                 ) | ||||
|             for r, g, b, a in pixels: | ||||
|                 R = r >> 3 | ||||
|                 G = g >> 2 | ||||
|                 B = b >> 3 | ||||
|                 rgb = (R << 11) | (G << 5) | B | ||||
|  | ||||
|                 if transparent: | ||||
|                     if rgb == 0x0020: | ||||
|                         rgb = 0 | ||||
|                     if a < 0x80: | ||||
|                         rgb = 0x0020 | ||||
|  | ||||
|                 data[pos] = rgb >> 8 | ||||
|                 pos += 1 | ||||
|                 data[pos] = rgb & 0xFF | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: | ||||
|         width8 = ((width + 7) // 8) * 8 | ||||
|         data = [0 for _ in range((height * width8 // 8) * frames)] | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             if transparent: | ||||
|                 alpha = image.split()[-1] | ||||
|                 has_alpha = alpha.getextrema()[0] < 0xFF | ||||
|             frame = image.convert("1", dither=Image.Dither.NONE) | ||||
|             if CONF_RESIZE in config: | ||||
|                 frame = frame.resize([width, height]) | ||||
|                 if transparent: | ||||
|                     alpha = alpha.resize([width, height]) | ||||
|             for x, y in [(i, j) for i in range(width) for j in range(height)]: | ||||
|                 if transparent and has_alpha: | ||||
|                     if not alpha.getpixel((x, y)): | ||||
|                         continue | ||||
|                 elif frame.getpixel((x, y)): | ||||
|                     continue | ||||
|  | ||||
|                 pos = x + y * width8 + (height * width8 * frameIndex) | ||||
|                 data[pos // 8] |= 0x80 >> (pos % 8) | ||||
|     else: | ||||
|         raise core.EsphomeError( | ||||
|             f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." | ||||
|         ) | ||||
|  | ||||
|     rhs = [HexInt(x) for x in data] | ||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||
|     var = cg.new_Pvariable( | ||||
|         config[CONF_ID], | ||||
|         prog_arr, | ||||
|         width, | ||||
|         height, | ||||
|         frames, | ||||
|         espImage.IMAGE_TYPE[config[CONF_TYPE]], | ||||
|         frame_count, | ||||
|         image_type, | ||||
|         trans_value, | ||||
|     ) | ||||
|     cg.add(var.set_transparency(transparent)) | ||||
|     if loop_config := config.get(CONF_LOOP): | ||||
|         start = loop_config[CONF_START_FRAME] | ||||
|         end = loop_config.get(CONF_END_FRAME, frames) | ||||
|         end = loop_config.get(CONF_END_FRAME, frame_count) | ||||
|         count = loop_config.get(CONF_REPEAT, -1) | ||||
|         cg.add(var.set_loop(start, end, count)) | ||||
|   | ||||
| @@ -6,8 +6,8 @@ namespace esphome { | ||||
| namespace animation { | ||||
|  | ||||
| Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, | ||||
|                      image::ImageType type) | ||||
|     : Image(data_start, width, height, type), | ||||
|                      image::ImageType type, image::Transparency transparent) | ||||
|     : Image(data_start, width, height, type, transparent), | ||||
|       animation_data_start_(data_start), | ||||
|       current_frame_(0), | ||||
|       animation_frame_count_(animation_frame_count), | ||||
| @@ -62,7 +62,7 @@ void Animation::set_frame(int frame) { | ||||
| } | ||||
|  | ||||
| void Animation::update_data_start_() { | ||||
|   const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_; | ||||
|   const uint32_t image_size = this->get_width_stride() * this->height_; | ||||
|   this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,8 @@ namespace animation { | ||||
|  | ||||
| class Animation : public image::Image { | ||||
|  public: | ||||
|   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); | ||||
|   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, | ||||
|             image::Transparency transparent); | ||||
|  | ||||
|   uint32_t get_animation_frame_count() const; | ||||
|   int get_current_frame() const; | ||||
|   | ||||
							
								
								
									
										4
									
								
								esphome/components/apds9306/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/apds9306/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # Based on this datasheet: | ||||
| # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| CODEOWNERS = ["@aodrenah"] | ||||
							
								
								
									
										152
									
								
								esphome/components/apds9306/apds9306.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								esphome/components/apds9306/apds9306.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| // Based on this datasheet: | ||||
| // https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| #include "apds9306.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9306 { | ||||
|  | ||||
| static const char *const TAG = "apds9306"; | ||||
|  | ||||
| enum {  // APDS9306 registers | ||||
|   APDS9306_MAIN_CTRL = 0x00, | ||||
|   APDS9306_ALS_MEAS_RATE = 0x04, | ||||
|   APDS9306_ALS_GAIN = 0x05, | ||||
|   APDS9306_PART_ID = 0x06, | ||||
|   APDS9306_MAIN_STATUS = 0x07, | ||||
|   APDS9306_CLEAR_DATA_0 = 0x0A,  // LSB | ||||
|   APDS9306_CLEAR_DATA_1 = 0x0B, | ||||
|   APDS9306_CLEAR_DATA_2 = 0x0C,  // MSB | ||||
|   APDS9306_ALS_DATA_0 = 0x0D,    // LSB | ||||
|   APDS9306_ALS_DATA_1 = 0x0E, | ||||
|   APDS9306_ALS_DATA_2 = 0x0F,  // MSB | ||||
|   APDS9306_INT_CFG = 0x19, | ||||
|   APDS9306_INT_PERSISTENCE = 0x1A, | ||||
|   APDS9306_ALS_THRES_UP_0 = 0x21,  // LSB | ||||
|   APDS9306_ALS_THRES_UP_1 = 0x22, | ||||
|   APDS9306_ALS_THRES_UP_2 = 0x23,   // MSB | ||||
|   APDS9306_ALS_THRES_LOW_0 = 0x24,  // LSB | ||||
|   APDS9306_ALS_THRES_LOW_1 = 0x25, | ||||
|   APDS9306_ALS_THRES_LOW_2 = 0x26,  // MSB | ||||
|   APDS9306_ALS_THRES_VAR = 0x27 | ||||
| }; | ||||
|  | ||||
| #define APDS9306_ERROR_CHECK(func, error) \ | ||||
|   if (!(func)) { \ | ||||
|     ESP_LOGE(TAG, error); \ | ||||
|     this->mark_failed(); \ | ||||
|     return; \ | ||||
|   } | ||||
| #define APDS9306_WARNING_CHECK(func, warning) \ | ||||
|   if (!(func)) { \ | ||||
|     ESP_LOGW(TAG, warning); \ | ||||
|     this->status_set_warning(); \ | ||||
|     return; \ | ||||
|   } | ||||
| #define APDS9306_WRITE_BYTE(reg, value) \ | ||||
|   ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \ | ||||
|   if (!this->write_byte(reg, value)) { \ | ||||
|     ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \ | ||||
|     this->mark_failed(); \ | ||||
|     return; \ | ||||
|   } | ||||
|  | ||||
| void APDS9306::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up APDS9306..."); | ||||
|  | ||||
|   uint8_t id; | ||||
|   if (!this->read_byte(APDS9306_PART_ID, &id)) {  // Part ID register | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (id != 0xB1 && id != 0xB3) {  // 0xB1 for APDS9306 0xB3 for APDS9306-065 | ||||
|     this->error_code_ = WRONG_ID; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // ALS resolution and measurement, see datasheet or init.py for options | ||||
|   uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07); | ||||
|   APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate); | ||||
|  | ||||
|   // ALS gain, see datasheet or init.py for options | ||||
|   uint8_t als_gain = (this->gain_ & 0x07); | ||||
|   APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain); | ||||
|  | ||||
|   // Set to standby mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); | ||||
|  | ||||
|   // Check for data, clear main status | ||||
|   uint8_t status; | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   // Set to active mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "APDS9306 setup complete"); | ||||
| } | ||||
|  | ||||
| void APDS9306::dump_config() { | ||||
|   LOG_SENSOR("", "APDS9306", this); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     switch (this->error_code_) { | ||||
|       case COMMUNICATION_FAILED: | ||||
|         ESP_LOGE(TAG, "Communication with APDS9306 failed!"); | ||||
|         break; | ||||
|       case WRONG_ID: | ||||
|         ESP_LOGE(TAG, "APDS9306 has invalid id!"); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGE(TAG, "Setting up APDS9306 registers failed!"); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]); | ||||
|   ESP_LOGCONFIG(TAG, "  Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]); | ||||
|   ESP_LOGCONFIG(TAG, "  Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]); | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| void APDS9306::update() { | ||||
|   // Check for new data | ||||
|   uint8_t status; | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
|  | ||||
|   status &= 0b00001000; | ||||
|   if (!status) {  // No new data | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set to standby mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); | ||||
|  | ||||
|   // Clear MAIN STATUS | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   uint8_t als_data[3]; | ||||
|   APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed."); | ||||
|  | ||||
|   // Set to active mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); | ||||
|  | ||||
|   uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]); | ||||
|  | ||||
|   float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) * | ||||
|               (100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]); | ||||
|  | ||||
|   ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux); | ||||
|   this->publish_state(lux); | ||||
| } | ||||
|  | ||||
| }  // namespace apds9306 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										66
									
								
								esphome/components/apds9306/apds9306.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/apds9306/apds9306.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| // Based on this datasheet: | ||||
| // https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9306 { | ||||
|  | ||||
| enum MeasurementBitWidth : uint8_t { | ||||
|   MEASUREMENT_BIT_WIDTH_20 = 0, | ||||
|   MEASUREMENT_BIT_WIDTH_19 = 1, | ||||
|   MEASUREMENT_BIT_WIDTH_18 = 2, | ||||
|   MEASUREMENT_BIT_WIDTH_17 = 3, | ||||
|   MEASUREMENT_BIT_WIDTH_16 = 4, | ||||
|   MEASUREMENT_BIT_WIDTH_13 = 5, | ||||
| }; | ||||
| static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13}; | ||||
|  | ||||
| enum MeasurementRate : uint8_t { | ||||
|   MEASUREMENT_RATE_25 = 0, | ||||
|   MEASUREMENT_RATE_50 = 1, | ||||
|   MEASUREMENT_RATE_100 = 2, | ||||
|   MEASUREMENT_RATE_200 = 3, | ||||
|   MEASUREMENT_RATE_500 = 4, | ||||
|   MEASUREMENT_RATE_1000 = 5, | ||||
|   MEASUREMENT_RATE_2000 = 6, | ||||
| }; | ||||
| static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000}; | ||||
|  | ||||
| enum AmbientLightGain : uint8_t { | ||||
|   AMBIENT_LIGHT_GAIN_1 = 0, | ||||
|   AMBIENT_LIGHT_GAIN_3 = 1, | ||||
|   AMBIENT_LIGHT_GAIN_6 = 2, | ||||
|   AMBIENT_LIGHT_GAIN_9 = 3, | ||||
|   AMBIENT_LIGHT_GAIN_18 = 4, | ||||
| }; | ||||
| static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18}; | ||||
|  | ||||
| class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   float get_setup_priority() const override { return setup_priority::BUS; } | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|   void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; } | ||||
|   void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; } | ||||
|   void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; } | ||||
|  | ||||
|  protected: | ||||
|   enum ErrorCode { | ||||
|     NONE = 0, | ||||
|     COMMUNICATION_FAILED, | ||||
|     WRONG_ID, | ||||
|   } error_code_{NONE}; | ||||
|  | ||||
|   MeasurementBitWidth bit_width_; | ||||
|   MeasurementRate measurement_rate_; | ||||
|   AmbientLightGain gain_; | ||||
| }; | ||||
|  | ||||
| }  // namespace apds9306 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										95
									
								
								esphome/components/apds9306/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								esphome/components/apds9306/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| # Based on this datasheet: | ||||
| # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_GAIN, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     ICON_LIGHTBULB, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_LUX, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| CONF_APDS9306_ID = "apds9306_id" | ||||
| CONF_BIT_WIDTH = "bit_width" | ||||
| CONF_MEASUREMENT_RATE = "measurement_rate" | ||||
|  | ||||
| apds9306_ns = cg.esphome_ns.namespace("apds9306") | ||||
| APDS9306 = apds9306_ns.class_( | ||||
|     "APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth") | ||||
| MeasurementRate = apds9306_ns.enum("MeasurementRate") | ||||
| AmbientLightGain = apds9306_ns.enum("AmbientLightGain") | ||||
|  | ||||
| MEASUREMENT_BIT_WIDTHS = { | ||||
|     20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20, | ||||
|     19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19, | ||||
|     18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18, | ||||
|     17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17, | ||||
|     16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16, | ||||
|     13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13, | ||||
| } | ||||
|  | ||||
| MEASUREMENT_RATES = { | ||||
|     25: MeasurementRate.MEASUREMENT_RATE_25, | ||||
|     50: MeasurementRate.MEASUREMENT_RATE_50, | ||||
|     100: MeasurementRate.MEASUREMENT_RATE_100, | ||||
|     200: MeasurementRate.MEASUREMENT_RATE_200, | ||||
|     500: MeasurementRate.MEASUREMENT_RATE_500, | ||||
|     1000: MeasurementRate.MEASUREMENT_RATE_1000, | ||||
|     2000: MeasurementRate.MEASUREMENT_RATE_2000, | ||||
| } | ||||
|  | ||||
| AMBIENT_LIGHT_GAINS = { | ||||
|     1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1, | ||||
|     3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3, | ||||
|     6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6, | ||||
|     9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9, | ||||
|     18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18, | ||||
| } | ||||
|  | ||||
|  | ||||
| def _validate_measurement_rate(value): | ||||
|     value = cv.positive_time_period_milliseconds(value) | ||||
|     return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         APDS9306, | ||||
|         unit_of_measurement=UNIT_LUX, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|         icon=ICON_LIGHTBULB, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True), | ||||
|             cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum( | ||||
|                 MEASUREMENT_BIT_WIDTHS, int=True | ||||
|             ), | ||||
|             cv.Optional( | ||||
|                 CONF_MEASUREMENT_RATE, default="100ms" | ||||
|             ): _validate_measurement_rate, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x52)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_bit_width(config[CONF_BIT_WIDTH])) | ||||
|     cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE])) | ||||
|     cg.add(var.set_ambient_light_gain(config[CONF_GAIN])) | ||||
| @@ -1,25 +1,27 @@ | ||||
| import base64 | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import Condition | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ACTION, | ||||
|     CONF_ACTIONS, | ||||
|     CONF_DATA, | ||||
|     CONF_DATA_TEMPLATE, | ||||
|     CONF_EVENT, | ||||
|     CONF_ID, | ||||
|     CONF_KEY, | ||||
|     CONF_ON_CLIENT_CONNECTED, | ||||
|     CONF_ON_CLIENT_DISCONNECTED, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PORT, | ||||
|     CONF_REBOOT_TIMEOUT, | ||||
|     CONF_SERVICE, | ||||
|     CONF_VARIABLES, | ||||
|     CONF_SERVICES, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_EVENT, | ||||
|     CONF_TAG, | ||||
|     CONF_ON_CLIENT_CONNECTED, | ||||
|     CONF_ON_CLIENT_DISCONNECTED, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VARIABLES, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
|  | ||||
| @@ -63,40 +65,51 @@ def validate_encryption_key(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
| ACTIONS_SCHEMA = automation.validate_automation( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(APIServer), | ||||
|         cv.Optional(CONF_PORT, default=6053): cv.port, | ||||
|         cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, | ||||
|         cv.Optional( | ||||
|             CONF_REBOOT_TIMEOUT, default="15min" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_SERVICES): automation.validate_automation( | ||||
|         cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), | ||||
|         cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, | ||||
|         cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, | ||||
|         cv.Optional(CONF_VARIABLES, default={}): cv.Schema( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), | ||||
|                 cv.Required(CONF_SERVICE): cv.valid_name, | ||||
|                 cv.Optional(CONF_VARIABLES, default={}): cv.Schema( | ||||
|                     { | ||||
|                         cv.validate_id_name: cv.one_of( | ||||
|                             *SERVICE_ARG_NATIVE_TYPES, lower=True | ||||
|                         ), | ||||
|                     } | ||||
|                 ), | ||||
|                 cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ENCRYPTION): cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_KEY): validate_encryption_key, | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||
|             single=True | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( | ||||
|             single=True | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|     }, | ||||
|     cv.All( | ||||
|         cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), | ||||
|         cv.rename_key(CONF_SERVICE, CONF_ACTION), | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(APIServer), | ||||
|             cv.Optional(CONF_PORT, default=6053): cv.port, | ||||
|             cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, | ||||
|             cv.Optional( | ||||
|                 CONF_REBOOT_TIMEOUT, default="15min" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|             cv.Exclusive( | ||||
|                 CONF_SERVICES, group_of_exclusion=CONF_ACTIONS | ||||
|             ): ACTIONS_SCHEMA, | ||||
|             cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, | ||||
|             cv.Optional(CONF_ENCRYPTION): cv.Schema( | ||||
|                 { | ||||
|                     cv.Required(CONF_KEY): validate_encryption_key, | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.rename_key(CONF_SERVICES, CONF_ACTIONS), | ||||
| ) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(40.0) | ||||
| @@ -108,7 +121,7 @@ async def to_code(config): | ||||
|     cg.add(var.set_password(config[CONF_PASSWORD])) | ||||
|     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||
|  | ||||
|     for conf in config.get(CONF_SERVICES, []): | ||||
|     for conf in config.get(CONF_ACTIONS, []): | ||||
|         template_args = [] | ||||
|         func_args = [] | ||||
|         service_arg_names = [] | ||||
| @@ -119,7 +132,7 @@ async def to_code(config): | ||||
|             service_arg_names.append(name) | ||||
|         templ = cg.TemplateArguments(*template_args) | ||||
|         trigger = cg.new_Pvariable( | ||||
|             conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names | ||||
|             conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names | ||||
|         ) | ||||
|         cg.add(var.register_user_service(trigger)) | ||||
|         await automation.build_automation(trigger, func_args, conf) | ||||
| @@ -142,7 +155,7 @@ async def to_code(config): | ||||
|         decoded = base64.b64decode(encryption_config[CONF_KEY]) | ||||
|         cg.add(var.set_noise_psk(list(decoded))) | ||||
|         cg.add_define("USE_API_NOISE") | ||||
|         cg.add_library("esphome/noise-c", "0.1.4") | ||||
|         cg.add_library("esphome/noise-c", "0.1.6") | ||||
|     else: | ||||
|         cg.add_define("USE_API_PLAINTEXT") | ||||
|  | ||||
| @@ -152,28 +165,43 @@ async def to_code(config): | ||||
|  | ||||
| KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) | ||||
|  | ||||
| HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(APIServer), | ||||
|         cv.Required(CONF_SERVICE): cv.templatable(cv.string), | ||||
|         cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, | ||||
|         cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, | ||||
|         cv.Optional(CONF_VARIABLES, default={}): cv.Schema( | ||||
|             {cv.string: cv.returning_lambda} | ||||
|         ), | ||||
|     } | ||||
|  | ||||
| HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.use_id(APIServer), | ||||
|             cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable( | ||||
|                 cv.string | ||||
|             ), | ||||
|             cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable( | ||||
|                 cv.string | ||||
|             ), | ||||
|             cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, | ||||
|             cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, | ||||
|             cv.Optional(CONF_VARIABLES, default={}): cv.Schema( | ||||
|                 {cv.string: cv.returning_lambda} | ||||
|             ), | ||||
|         } | ||||
|     ), | ||||
|     cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), | ||||
|     cv.rename_key(CONF_SERVICE, CONF_ACTION), | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "homeassistant.action", | ||||
|     HomeAssistantServiceCallAction, | ||||
|     HOMEASSISTANT_ACTION_ACTION_SCHEMA, | ||||
| ) | ||||
| @automation.register_action( | ||||
|     "homeassistant.service", | ||||
|     HomeAssistantServiceCallAction, | ||||
|     HOMEASSISTANT_SERVICE_ACTION_SCHEMA, | ||||
|     HOMEASSISTANT_ACTION_ACTION_SCHEMA, | ||||
| ) | ||||
| async def homeassistant_service_to_code(config, action_id, template_arg, args): | ||||
|     serv = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, False) | ||||
|     templ = await cg.templatable(config[CONF_SERVICE], args, None) | ||||
|     templ = await cg.templatable(config[CONF_ACTION], args, None) | ||||
|     cg.add(var.set_service(templ)) | ||||
|     for key, value in config[CONF_DATA].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|   | ||||
| @@ -62,6 +62,8 @@ service APIConnection { | ||||
|   rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} | ||||
|  | ||||
|   rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} | ||||
|   rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} | ||||
|   rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {} | ||||
|  | ||||
|   rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} | ||||
| } | ||||
| @@ -225,6 +227,9 @@ message DeviceInfoResponse { | ||||
|   uint32 voice_assistant_feature_flags = 17; | ||||
|  | ||||
|   string suggested_area = 16; | ||||
|  | ||||
|   // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" | ||||
|   string bluetooth_mac_address = 18; | ||||
| } | ||||
|  | ||||
| message ListEntitiesRequest { | ||||
| @@ -686,6 +691,7 @@ message SubscribeHomeAssistantStateResponse { | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   string entity_id = 1; | ||||
|   string attribute = 2; | ||||
|   bool once = 3; | ||||
| } | ||||
|  | ||||
| message HomeAssistantStateResponse { | ||||
| @@ -1106,6 +1112,19 @@ enum MediaPlayerCommand { | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3; | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4; | ||||
| } | ||||
| enum MediaPlayerFormatPurpose { | ||||
|   MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; | ||||
|   MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1; | ||||
| } | ||||
| message MediaPlayerSupportedFormat { | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|  | ||||
|   string format = 1; | ||||
|   uint32 sample_rate = 2; | ||||
|   uint32 num_channels = 3; | ||||
|   MediaPlayerFormatPurpose purpose = 4; | ||||
|   uint32 sample_bytes = 5; | ||||
| } | ||||
| message ListEntitiesMediaPlayerResponse { | ||||
|   option (id) = 63; | ||||
|   option (source) = SOURCE_SERVER; | ||||
| @@ -1121,6 +1140,8 @@ message ListEntitiesMediaPlayerResponse { | ||||
|   EntityCategory entity_category = 7; | ||||
|  | ||||
|   bool supports_pause = 8; | ||||
|  | ||||
|   repeated MediaPlayerSupportedFormat supported_formats = 9; | ||||
| } | ||||
| message MediaPlayerStateResponse { | ||||
|   option (id) = 64; | ||||
| @@ -1363,6 +1384,7 @@ message BluetoothConnectionsFreeResponse { | ||||
|  | ||||
|   uint32 free = 1; | ||||
|   uint32 limit = 2; | ||||
|   repeated uint64 allocated = 3; | ||||
| } | ||||
|  | ||||
| message BluetoothGATTErrorResponse { | ||||
| @@ -1538,6 +1560,55 @@ message VoiceAssistantTimerEventResponse { | ||||
|   bool is_active = 6; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantAnnounceRequest { | ||||
|   option (id) = 119; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||
|  | ||||
|   string media_id = 1; | ||||
|   string text = 2; | ||||
|   string preannounce_media_id = 3; | ||||
|   bool start_conversation = 4; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantAnnounceFinished { | ||||
|   option (id) = 120; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||
|  | ||||
|   bool success = 1; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantWakeWord { | ||||
|   string id = 1; | ||||
|   string wake_word = 2; | ||||
|   repeated string trained_languages = 3; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantConfigurationRequest { | ||||
|   option (id) = 121; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantConfigurationResponse { | ||||
|   option (id) = 122; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||
|  | ||||
|   repeated VoiceAssistantWakeWord available_wake_words = 1; | ||||
|   repeated string active_wake_words = 2; | ||||
|   uint32 max_active_wake_words = 3; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantSetConfiguration { | ||||
|   option (id) = 123; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||
|  | ||||
|   repeated string active_wake_words = 1; | ||||
| } | ||||
|  | ||||
| // ==================== ALARM CONTROL PANEL ==================== | ||||
| enum AlarmControlPanelState { | ||||
|   ALARM_STATE_DISARMED = 0; | ||||
| @@ -1872,6 +1943,11 @@ message UpdateStateResponse { | ||||
|   string release_summary = 9; | ||||
|   string release_url = 10; | ||||
| } | ||||
| enum UpdateCommand { | ||||
|   UPDATE_COMMAND_NONE = 0; | ||||
|   UPDATE_COMMAND_UPDATE = 1; | ||||
|   UPDATE_COMMAND_CHECK = 2; | ||||
| } | ||||
| message UpdateCommandRequest { | ||||
|   option (id) = 118; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
| @@ -1879,5 +1955,5 @@ message UpdateCommandRequest { | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   bool install = 2; | ||||
|   UpdateCommand command = 2; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,18 +1,59 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_API | ||||
| #include "api_frame_helper.h" | ||||
| #include "api_pb2.h" | ||||
| #include "api_pb2_service.h" | ||||
| #include "api_server.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| using send_message_t = bool(APIConnection *, void *); | ||||
|  | ||||
| /* | ||||
|   This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that | ||||
|   will lazily publish that message.  The two pointers allow dedup in the deferred queue if multiple publishes for the | ||||
|   same component are backed up, and take up only 8 bytes of memory.  The entry in the deferred queue (a std::vector) is | ||||
|   the DeferredMessage instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per entry.  Even | ||||
|   100 backed up messages (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8 | ||||
|   kB. | ||||
| */ | ||||
| class DeferredMessageQueue { | ||||
|   struct DeferredMessage { | ||||
|     friend class DeferredMessageQueue; | ||||
|  | ||||
|    protected: | ||||
|     void *source_; | ||||
|     send_message_t *send_message_; | ||||
|  | ||||
|    public: | ||||
|     DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {} | ||||
|     bool operator==(const DeferredMessage &test) const { | ||||
|       return (source_ == test.source_ && send_message_ == test.send_message_); | ||||
|     } | ||||
|   } __attribute__((packed)); | ||||
|  | ||||
|  protected: | ||||
|   // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory | ||||
|   // footprint is more important than speed here) | ||||
|   std::vector<DeferredMessage> deferred_queue_; | ||||
|   APIConnection *api_connection_; | ||||
|  | ||||
|   // helper for allowing only unique entries in the queue | ||||
|   void dmq_push_back_with_dedup_(void *source, send_message_t *send_message); | ||||
|  | ||||
|  public: | ||||
|   DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {} | ||||
|   void process_queue(); | ||||
|   void defer(void *source, send_message_t *send_message); | ||||
| }; | ||||
|  | ||||
| class APIConnection : public APIServerConnection { | ||||
|  public: | ||||
|   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); | ||||
| @@ -27,96 +68,140 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); | ||||
|   bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); | ||||
|   void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); | ||||
|   static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor); | ||||
|   static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state); | ||||
|   static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor); | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
|   bool send_cover_state(cover::Cover *cover); | ||||
|   bool send_cover_info(cover::Cover *cover); | ||||
|   void send_cover_info(cover::Cover *cover); | ||||
|   static bool try_send_cover_state(APIConnection *api, void *v_cover); | ||||
|   static bool try_send_cover_info(APIConnection *api, void *v_cover); | ||||
|   void cover_command(const CoverCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   bool send_fan_state(fan::Fan *fan); | ||||
|   bool send_fan_info(fan::Fan *fan); | ||||
|   void send_fan_info(fan::Fan *fan); | ||||
|   static bool try_send_fan_state(APIConnection *api, void *v_fan); | ||||
|   static bool try_send_fan_info(APIConnection *api, void *v_fan); | ||||
|   void fan_command(const FanCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|   bool send_light_state(light::LightState *light); | ||||
|   bool send_light_info(light::LightState *light); | ||||
|   void send_light_info(light::LightState *light); | ||||
|   static bool try_send_light_state(APIConnection *api, void *v_light); | ||||
|   static bool try_send_light_info(APIConnection *api, void *v_light); | ||||
|   void light_command(const LightCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   bool send_sensor_state(sensor::Sensor *sensor, float state); | ||||
|   bool send_sensor_info(sensor::Sensor *sensor); | ||||
|   void send_sensor_info(sensor::Sensor *sensor); | ||||
|   static bool try_send_sensor_state(APIConnection *api, void *v_sensor); | ||||
|   static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state); | ||||
|   static bool try_send_sensor_info(APIConnection *api, void *v_sensor); | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|   bool send_switch_state(switch_::Switch *a_switch, bool state); | ||||
|   bool send_switch_info(switch_::Switch *a_switch); | ||||
|   void send_switch_info(switch_::Switch *a_switch); | ||||
|   static bool try_send_switch_state(APIConnection *api, void *v_a_switch); | ||||
|   static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state); | ||||
|   static bool try_send_switch_info(APIConnection *api, void *v_a_switch); | ||||
|   void switch_command(const SwitchCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); | ||||
|   bool send_text_sensor_info(text_sensor::TextSensor *text_sensor); | ||||
|   void send_text_sensor_info(text_sensor::TextSensor *text_sensor); | ||||
|   static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor); | ||||
|   static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state); | ||||
|   static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor); | ||||
| #endif | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); | ||||
|   bool send_camera_info(esp32_camera::ESP32Camera *camera); | ||||
|   void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); | ||||
|   void send_camera_info(esp32_camera::ESP32Camera *camera); | ||||
|   static bool try_send_camera_info(APIConnection *api, void *v_camera); | ||||
|   void camera_image(const CameraImageRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
|   bool send_climate_state(climate::Climate *climate); | ||||
|   bool send_climate_info(climate::Climate *climate); | ||||
|   void send_climate_info(climate::Climate *climate); | ||||
|   static bool try_send_climate_state(APIConnection *api, void *v_climate); | ||||
|   static bool try_send_climate_info(APIConnection *api, void *v_climate); | ||||
|   void climate_command(const ClimateCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   bool send_number_state(number::Number *number, float state); | ||||
|   bool send_number_info(number::Number *number); | ||||
|   void send_number_info(number::Number *number); | ||||
|   static bool try_send_number_state(APIConnection *api, void *v_number); | ||||
|   static bool try_send_number_state(APIConnection *api, number::Number *number, float state); | ||||
|   static bool try_send_number_info(APIConnection *api, void *v_number); | ||||
|   void number_command(const NumberCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATE | ||||
|   bool send_date_state(datetime::DateEntity *date); | ||||
|   bool send_date_info(datetime::DateEntity *date); | ||||
|   void send_date_info(datetime::DateEntity *date); | ||||
|   static bool try_send_date_state(APIConnection *api, void *v_date); | ||||
|   static bool try_send_date_info(APIConnection *api, void *v_date); | ||||
|   void date_command(const DateCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_TIME | ||||
|   bool send_time_state(datetime::TimeEntity *time); | ||||
|   bool send_time_info(datetime::TimeEntity *time); | ||||
|   void send_time_info(datetime::TimeEntity *time); | ||||
|   static bool try_send_time_state(APIConnection *api, void *v_time); | ||||
|   static bool try_send_time_info(APIConnection *api, void *v_time); | ||||
|   void time_command(const TimeCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|   bool send_datetime_state(datetime::DateTimeEntity *datetime); | ||||
|   bool send_datetime_info(datetime::DateTimeEntity *datetime); | ||||
|   void send_datetime_info(datetime::DateTimeEntity *datetime); | ||||
|   static bool try_send_datetime_state(APIConnection *api, void *v_datetime); | ||||
|   static bool try_send_datetime_info(APIConnection *api, void *v_datetime); | ||||
|   void datetime_command(const DateTimeCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_TEXT | ||||
|   bool send_text_state(text::Text *text, std::string state); | ||||
|   bool send_text_info(text::Text *text); | ||||
|   void send_text_info(text::Text *text); | ||||
|   static bool try_send_text_state(APIConnection *api, void *v_text); | ||||
|   static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state); | ||||
|   static bool try_send_text_info(APIConnection *api, void *v_text); | ||||
|   void text_command(const TextCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   bool send_select_state(select::Select *select, std::string state); | ||||
|   bool send_select_info(select::Select *select); | ||||
|   void send_select_info(select::Select *select); | ||||
|   static bool try_send_select_state(APIConnection *api, void *v_select); | ||||
|   static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state); | ||||
|   static bool try_send_select_info(APIConnection *api, void *v_select); | ||||
|   void select_command(const SelectCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   bool send_button_info(button::Button *button); | ||||
|   void send_button_info(button::Button *button); | ||||
|   static bool try_send_button_info(APIConnection *api, void *v_button); | ||||
|   void button_command(const ButtonCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool send_lock_state(lock::Lock *a_lock, lock::LockState state); | ||||
|   bool send_lock_info(lock::Lock *a_lock); | ||||
|   void send_lock_info(lock::Lock *a_lock); | ||||
|   static bool try_send_lock_state(APIConnection *api, void *v_a_lock); | ||||
|   static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state); | ||||
|   static bool try_send_lock_info(APIConnection *api, void *v_a_lock); | ||||
|   void lock_command(const LockCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
|   bool send_valve_state(valve::Valve *valve); | ||||
|   bool send_valve_info(valve::Valve *valve); | ||||
|   void send_valve_info(valve::Valve *valve); | ||||
|   static bool try_send_valve_state(APIConnection *api, void *v_valve); | ||||
|   static bool try_send_valve_info(APIConnection *api, void *v_valve); | ||||
|   void valve_command(const ValveCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_media_player_state(media_player::MediaPlayer *media_player); | ||||
|   bool send_media_player_info(media_player::MediaPlayer *media_player); | ||||
|   void send_media_player_info(media_player::MediaPlayer *media_player); | ||||
|   static bool try_send_media_player_state(APIConnection *api, void *v_media_player); | ||||
|   static bool try_send_media_player_info(APIConnection *api, void *v_media_player); | ||||
|   void media_player_command(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool send_log_message(int level, const char *tag, const char *line); | ||||
|   bool try_send_log_message(int level, const char *tag, const char *line); | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|     if (!this->service_call_subscription_) | ||||
|       return; | ||||
| @@ -151,22 +236,33 @@ class APIConnection : public APIServerConnection { | ||||
|   void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; | ||||
|   void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; | ||||
|   void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; | ||||
|   void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; | ||||
|   VoiceAssistantConfigurationResponse voice_assistant_get_configuration( | ||||
|       const VoiceAssistantConfigurationRequest &msg) override; | ||||
|   void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel); | ||||
|   static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel); | ||||
|   void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
|   bool send_event(event::Event *event, std::string event_type); | ||||
|   bool send_event_info(event::Event *event); | ||||
|   void send_event(event::Event *event, std::string event_type); | ||||
|   void send_event_info(event::Event *event); | ||||
|   static bool try_send_event(APIConnection *api, void *v_event); | ||||
|   static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type); | ||||
|   static bool try_send_event_info(APIConnection *api, void *v_event); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|   bool send_update_state(update::UpdateEntity *update); | ||||
|   bool send_update_info(update::UpdateEntity *update); | ||||
|   void send_update_info(update::UpdateEntity *update); | ||||
|   static bool try_send_update_state(APIConnection *api, void *v_update); | ||||
|   static bool try_send_update_info(APIConnection *api, void *v_update); | ||||
|   void update_command(const UpdateCommandRequest &msg) override; | ||||
| #endif | ||||
|  | ||||
| @@ -257,6 +353,7 @@ class APIConnection : public APIServerConnection { | ||||
|   bool service_call_subscription_{false}; | ||||
|   bool next_close_ = false; | ||||
|   APIServer *parent_; | ||||
|   DeferredMessageQueue deferred_message_queue_; | ||||
|   InitialStateIterator initial_state_iterator_; | ||||
|   ListEntitiesIterator list_entities_iterator_; | ||||
|   int state_subs_at_ = -1; | ||||
| @@ -264,3 +361,4 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| #include "api_frame_helper.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| @@ -1028,3 +1028,4 @@ APIError APIPlaintextFrameHelper::shutdown(int how) { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_NOISE | ||||
| #include "noise/protocol.h" | ||||
| #endif | ||||
| @@ -190,3 +190,4 @@ class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -387,6 +387,18 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me | ||||
| } | ||||
| #endif | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| template<> const char *proto_enum_to_string<enums::MediaPlayerFormatPurpose>(enums::MediaPlayerFormatPurpose value) { | ||||
|   switch (value) { | ||||
|     case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: | ||||
|       return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; | ||||
|     case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: | ||||
|       return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| template<> | ||||
| const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::BluetoothDeviceRequestType value) { | ||||
|   switch (value) { | ||||
| @@ -567,6 +579,20 @@ template<> const char *proto_enum_to_string<enums::ValveOperation>(enums::ValveO | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateCommand value) { | ||||
|   switch (value) { | ||||
|     case enums::UPDATE_COMMAND_NONE: | ||||
|       return "UPDATE_COMMAND_NONE"; | ||||
|     case enums::UPDATE_COMMAND_UPDATE: | ||||
|       return "UPDATE_COMMAND_UPDATE"; | ||||
|     case enums::UPDATE_COMMAND_CHECK: | ||||
|       return "UPDATE_COMMAND_CHECK"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
| @@ -812,6 +838,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v | ||||
|       this->suggested_area = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 18: { | ||||
|       this->bluetooth_mac_address = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -834,6 +864,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(14, this->legacy_voice_assistant_version); | ||||
|   buffer.encode_uint32(17, this->voice_assistant_feature_flags); | ||||
|   buffer.encode_string(16, this->suggested_area); | ||||
|   buffer.encode_string(18, this->bluetooth_mac_address); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
| @@ -911,6 +942,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   out.append("  suggested_area: "); | ||||
|   out.append("'").append(this->suggested_area).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  bluetooth_mac_address: "); | ||||
|   out.append("'").append(this->bluetooth_mac_address).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3095,6 +3130,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { | ||||
|   out.append("SubscribeHomeAssistantStatesRequest {}"); | ||||
| } | ||||
| #endif | ||||
| bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 3: { | ||||
|       this->once = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
| @@ -3112,6 +3157,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto | ||||
| void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->entity_id); | ||||
|   buffer.encode_string(2, this->attribute); | ||||
|   buffer.encode_bool(3, this->once); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { | ||||
| @@ -3124,6 +3170,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { | ||||
|   out.append("  attribute: "); | ||||
|   out.append("'").append(this->attribute).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  once: "); | ||||
|   out.append(YESNO(this->once)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -5094,6 +5144,74 @@ void ButtonCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->sample_rate = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->num_channels = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->sample_bytes = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->format = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->format); | ||||
|   buffer.encode_uint32(2, this->sample_rate); | ||||
|   buffer.encode_uint32(3, this->num_channels); | ||||
|   buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose); | ||||
|   buffer.encode_uint32(5, this->sample_bytes); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void MediaPlayerSupportedFormat::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("MediaPlayerSupportedFormat {\n"); | ||||
|   out.append("  format: "); | ||||
|   out.append("'").append(this->format).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  sample_rate: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->sample_rate); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  num_channels: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->num_channels); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  purpose: "); | ||||
|   out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  sample_bytes: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->sample_bytes); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
| @@ -5130,6 +5248,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->supported_formats.push_back(value.as_message<MediaPlayerSupportedFormat>()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -5153,6 +5275,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_bool(8, this->supports_pause); | ||||
|   for (auto &it : this->supported_formats) { | ||||
|     buffer.encode_message<MediaPlayerSupportedFormat>(9, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
| @@ -5190,6 +5315,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
|   out.append("  supports_pause: "); | ||||
|   out.append(YESNO(this->supports_pause)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->supported_formats) { | ||||
|     out.append("  supported_formats: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -6308,6 +6439,10 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar | ||||
|       this->limit = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->allocated.push_back(value.as_uint64()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -6315,6 +6450,9 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar | ||||
| void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(1, this->free); | ||||
|   buffer.encode_uint32(2, this->limit); | ||||
|   for (auto &it : this->allocated) { | ||||
|     buffer.encode_uint64(3, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { | ||||
| @@ -6329,6 +6467,13 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { | ||||
|   sprintf(buffer, "%" PRIu32, this->limit); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->allocated) { | ||||
|     out.append("  allocated: "); | ||||
|     sprintf(buffer, "%llu", it); | ||||
|     out.append(buffer); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -6949,6 +7094,217 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 4: { | ||||
|       this->start_conversation = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->media_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 2: { | ||||
|       this->text = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->preannounce_media_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->media_id); | ||||
|   buffer.encode_string(2, this->text); | ||||
|   buffer.encode_string(3, this->preannounce_media_id); | ||||
|   buffer.encode_bool(4, this->start_conversation); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("VoiceAssistantAnnounceRequest {\n"); | ||||
|   out.append("  media_id: "); | ||||
|   out.append("'").append(this->media_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  text: "); | ||||
|   out.append("'").append(this->text).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  preannounce_media_id: "); | ||||
|   out.append("'").append(this->preannounce_media_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  start_conversation: "); | ||||
|   out.append(YESNO(this->start_conversation)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->success = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("VoiceAssistantAnnounceFinished {\n"); | ||||
|   out.append("  success: "); | ||||
|   out.append(YESNO(this->success)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 2: { | ||||
|       this->wake_word = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->trained_languages.push_back(value.as_string()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->id); | ||||
|   buffer.encode_string(2, this->wake_word); | ||||
|   for (auto &it : this->trained_languages) { | ||||
|     buffer.encode_string(3, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantWakeWord::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("VoiceAssistantWakeWord {\n"); | ||||
|   out.append("  id: "); | ||||
|   out.append("'").append(this->id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  wake_word: "); | ||||
|   out.append("'").append(this->wake_word).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->trained_languages) { | ||||
|     out.append("  trained_languages: "); | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { | ||||
|   out.append("VoiceAssistantConfigurationRequest {}"); | ||||
| } | ||||
| #endif | ||||
| bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 3: { | ||||
|       this->max_active_wake_words = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->available_wake_words.push_back(value.as_message<VoiceAssistantWakeWord>()); | ||||
|       return true; | ||||
|     } | ||||
|     case 2: { | ||||
|       this->active_wake_words.push_back(value.as_string()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (auto &it : this->available_wake_words) { | ||||
|     buffer.encode_message<VoiceAssistantWakeWord>(1, it, true); | ||||
|   } | ||||
|   for (auto &it : this->active_wake_words) { | ||||
|     buffer.encode_string(2, it, true); | ||||
|   } | ||||
|   buffer.encode_uint32(3, this->max_active_wake_words); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("VoiceAssistantConfigurationResponse {\n"); | ||||
|   for (const auto &it : this->available_wake_words) { | ||||
|     out.append("  available_wake_words: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   for (const auto &it : this->active_wake_words) { | ||||
|     out.append("  active_wake_words: "); | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   out.append("  max_active_wake_words: "); | ||||
|   sprintf(buffer, "%" PRIu32, this->max_active_wake_words); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->active_wake_words.push_back(value.as_string()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (auto &it : this->active_wake_words) { | ||||
|     buffer.encode_string(1, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("VoiceAssistantSetConfiguration {\n"); | ||||
|   for (const auto &it : this->active_wake_words) { | ||||
|     out.append("  active_wake_words: "); | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
| @@ -8596,7 +8952,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { | ||||
| bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->install = value.as_bool(); | ||||
|       this->command = value.as_enum<enums::UpdateCommand>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
| @@ -8615,7 +8971,7 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
| } | ||||
| void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_bool(2, this->install); | ||||
|   buffer.encode_enum<enums::UpdateCommand>(2, this->command); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void UpdateCommandRequest::dump_to(std::string &out) const { | ||||
| @@ -8626,8 +8982,8 @@ void UpdateCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  install: "); | ||||
|   out.append(YESNO(this->install)); | ||||
|   out.append("  command: "); | ||||
|   out.append(proto_enum_to_string<enums::UpdateCommand>(this->command)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
|   | ||||
| @@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t { | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3, | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4, | ||||
| }; | ||||
| enum MediaPlayerFormatPurpose : uint32_t { | ||||
|   MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, | ||||
|   MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1, | ||||
| }; | ||||
| enum BluetoothDeviceRequestType : uint32_t { | ||||
|   BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, | ||||
|   BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, | ||||
| @@ -227,6 +231,11 @@ enum ValveOperation : uint32_t { | ||||
|   VALVE_OPERATION_IS_OPENING = 1, | ||||
|   VALVE_OPERATION_IS_CLOSING = 2, | ||||
| }; | ||||
| enum UpdateCommand : uint32_t { | ||||
|   UPDATE_COMMAND_NONE = 0, | ||||
|   UPDATE_COMMAND_UPDATE = 1, | ||||
|   UPDATE_COMMAND_CHECK = 2, | ||||
| }; | ||||
|  | ||||
| }  // namespace enums | ||||
|  | ||||
| @@ -345,6 +354,7 @@ class DeviceInfoResponse : public ProtoMessage { | ||||
|   uint32_t legacy_voice_assistant_version{0}; | ||||
|   uint32_t voice_assistant_feature_flags{0}; | ||||
|   std::string suggested_area{}; | ||||
|   std::string bluetooth_mac_address{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -831,6 +841,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string entity_id{}; | ||||
|   std::string attribute{}; | ||||
|   bool once{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -838,6 +849,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class HomeAssistantStateResponse : public ProtoMessage { | ||||
|  public: | ||||
| @@ -1260,6 +1272,22 @@ class ButtonCommandRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| class MediaPlayerSupportedFormat : public ProtoMessage { | ||||
|  public: | ||||
|   std::string format{}; | ||||
|   uint32_t sample_rate{0}; | ||||
|   uint32_t num_channels{0}; | ||||
|   enums::MediaPlayerFormatPurpose purpose{}; | ||||
|   uint32_t sample_bytes{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
| @@ -1270,6 +1298,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_pause{false}; | ||||
|   std::vector<MediaPlayerSupportedFormat> supported_formats{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -1596,6 +1625,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t free{0}; | ||||
|   uint32_t limit{0}; | ||||
|   std::vector<uint64_t> allocated{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -1798,6 +1828,79 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class VoiceAssistantAnnounceRequest : public ProtoMessage { | ||||
|  public: | ||||
|   std::string media_id{}; | ||||
|   std::string text{}; | ||||
|   std::string preannounce_media_id{}; | ||||
|   bool start_conversation{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class VoiceAssistantAnnounceFinished : public ProtoMessage { | ||||
|  public: | ||||
|   bool success{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class VoiceAssistantWakeWord : public ProtoMessage { | ||||
|  public: | ||||
|   std::string id{}; | ||||
|   std::string wake_word{}; | ||||
|   std::vector<std::string> trained_languages{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class VoiceAssistantConfigurationRequest : public ProtoMessage { | ||||
|  public: | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| class VoiceAssistantConfigurationResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::vector<VoiceAssistantWakeWord> available_wake_words{}; | ||||
|   std::vector<std::string> active_wake_words{}; | ||||
|   uint32_t max_active_wake_words{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class VoiceAssistantSetConfiguration : public ProtoMessage { | ||||
|  public: | ||||
|   std::vector<std::string> active_wake_words{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
| @@ -2175,7 +2278,7 @@ class UpdateStateResponse : public ProtoMessage { | ||||
| class UpdateCommandRequest : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   bool install{false}; | ||||
|   enums::UpdateCommand command{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
|   | ||||
| @@ -486,6 +486,29 @@ bool APIServerConnectionBase::send_voice_assistant_audio(const VoiceAssistantAud | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| bool APIServerConnectionBase::send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_voice_assistant_announce_finished: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| bool APIServerConnectionBase::send_voice_assistant_configuration_response( | ||||
|     const VoiceAssistantConfigurationResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<VoiceAssistantConfigurationResponse>(msg, 122); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
| bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( | ||||
|     const ListEntitiesAlarmControlPanelResponse &msg) { | ||||
| @@ -1135,6 +1158,39 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_update_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 119: { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|       VoiceAssistantAnnounceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_voice_assistant_announce_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 121: { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|       VoiceAssistantConfigurationRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_voice_assistant_configuration_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 123: { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|       VoiceAssistantSetConfiguration msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_voice_assistant_set_configuration(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
| @@ -1625,6 +1681,35 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo | ||||
|   this->subscribe_voice_assistant(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||
|   if (!this->send_voice_assistant_configuration_response(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->voice_assistant_set_configuration(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
| void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|   | ||||
| @@ -247,6 +247,21 @@ class APIServerConnectionBase : public ProtoService { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg); | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); | ||||
| #endif | ||||
| @@ -419,6 +434,13 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( | ||||
|       const VoiceAssistantConfigurationRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; | ||||
| #endif | ||||
| @@ -520,6 +542,12 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; | ||||
| #endif | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #include <cerrno> | ||||
| #include "api_connection.h" | ||||
| #include "esphome/components/network/util.h" | ||||
| @@ -71,7 +72,7 @@ void APIServer::setup() { | ||||
|     logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { | ||||
|       for (auto &c : this->clients_) { | ||||
|         if (!c->remove_) | ||||
|           c->send_log_message(level, tag, message); | ||||
|           c->try_send_log_message(level, tag, message); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| @@ -85,7 +86,7 @@ void APIServer::setup() { | ||||
|         [this](const std::shared_ptr<esp32_camera::CameraImage> &image) { | ||||
|           for (auto &c : this->clients_) { | ||||
|             if (!c->remove_) | ||||
|               c->send_camera_state(image); | ||||
|               c->set_camera_state(image); | ||||
|           } | ||||
|         }); | ||||
|   } | ||||
| @@ -359,8 +360,18 @@ void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<s | ||||
|       .entity_id = std::move(entity_id), | ||||
|       .attribute = std::move(attribute), | ||||
|       .callback = std::move(f), | ||||
|       .once = false, | ||||
|   }); | ||||
| } | ||||
| void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                          std::function<void(std::string)> f) { | ||||
|   this->state_subs_.push_back(HomeAssistantStateSubscription{ | ||||
|       .entity_id = std::move(entity_id), | ||||
|       .attribute = std::move(attribute), | ||||
|       .callback = std::move(f), | ||||
|       .once = true, | ||||
|   }); | ||||
| }; | ||||
| const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { | ||||
|   return this->state_subs_; | ||||
| } | ||||
| @@ -393,3 +404,4 @@ void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_API | ||||
| #include "api_noise_context.h" | ||||
| #include "api_pb2.h" | ||||
| #include "api_pb2_service.h" | ||||
| @@ -7,7 +9,6 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/controller.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "list_entities.h" | ||||
| #include "subscribe_state.h" | ||||
| @@ -112,10 +113,13 @@ class APIServer : public Component, public Controller { | ||||
|     std::string entity_id; | ||||
|     optional<std::string> attribute; | ||||
|     std::function<void(std::string)> callback; | ||||
|     bool once; | ||||
|   }; | ||||
|  | ||||
|   void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                       std::function<void(std::string)> f); | ||||
|   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                 std::function<void(std::string)> f); | ||||
|   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } | ||||
|  | ||||
| @@ -150,3 +154,4 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import logging | ||||
| from datetime import datetime | ||||
| from typing import Any | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| from aioesphomeapi import APIClient | ||||
| from aioesphomeapi.api_pb2 import SubscribeLogsResponse | ||||
| from aioesphomeapi.log_runner import async_run | ||||
|  | ||||
| from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | ||||
| @@ -14,6 +13,12 @@ from esphome.core import CORE | ||||
|  | ||||
| from . import CONF_ENCRYPTION | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from aioesphomeapi.api_pb2 import ( | ||||
|         SubscribeLogsResponse,  # pylint: disable=no-name-in-module | ||||
|     ) | ||||
|  | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <map> | ||||
| #include "user_services.h" | ||||
| #include "api_server.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| #include "user_services.h" | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -216,3 +216,4 @@ class CustomAPIDevice { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #include "api_pb2.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "api_pb2.h" | ||||
| #include "api_server.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -81,3 +81,4 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "list_entities.h" | ||||
| #ifdef USE_API | ||||
| #include "api_connection.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -9,37 +10,63 @@ namespace api { | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { | ||||
|   return this->client_->send_binary_sensor_info(binary_sensor); | ||||
|   this->client_->send_binary_sensor_info(binary_sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
| bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); } | ||||
| bool ListEntitiesIterator::on_cover(cover::Cover *cover) { | ||||
|   this->client_->send_cover_info(cover); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| bool ListEntitiesIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_info(fan); } | ||||
| bool ListEntitiesIterator::on_fan(fan::Fan *fan) { | ||||
|   this->client_->send_fan_info(fan); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); } | ||||
| bool ListEntitiesIterator::on_light(light::LightState *light) { | ||||
|   this->client_->send_light_info(light); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
| bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); } | ||||
| bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { | ||||
|   this->client_->send_sensor_info(sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
| bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); } | ||||
| bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { | ||||
|   this->client_->send_switch_info(a_switch); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| bool ListEntitiesIterator::on_button(button::Button *button) { return this->client_->send_button_info(button); } | ||||
| bool ListEntitiesIterator::on_button(button::Button *button) { | ||||
|   this->client_->send_button_info(button); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
| bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { | ||||
|   return this->client_->send_text_sensor_info(text_sensor); | ||||
|   this->client_->send_text_sensor_info(text_sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); } | ||||
| bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { | ||||
|   this->client_->send_lock_info(a_lock); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
| bool ListEntitiesIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_info(valve); } | ||||
| bool ListEntitiesIterator::on_valve(valve::Valve *valve) { | ||||
|   this->client_->send_valve_info(valve); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } | ||||
| @@ -51,56 +78,85 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { | ||||
|   return this->client_->send_camera_info(camera); | ||||
|   this->client_->send_camera_info(camera); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
| bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); } | ||||
| bool ListEntitiesIterator::on_climate(climate::Climate *climate) { | ||||
|   this->client_->send_climate_info(climate); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } | ||||
| bool ListEntitiesIterator::on_number(number::Number *number) { | ||||
|   this->client_->send_number_info(number); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATE | ||||
| bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); } | ||||
| bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { | ||||
|   this->client_->send_date_info(date); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_TIME | ||||
| bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); } | ||||
| bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { | ||||
|   this->client_->send_time_info(time); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
| bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { | ||||
|   return this->client_->send_datetime_info(datetime); | ||||
|   this->client_->send_datetime_info(datetime); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
| bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } | ||||
| bool ListEntitiesIterator::on_text(text::Text *text) { | ||||
|   this->client_->send_text_info(text); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { | ||||
|   this->client_->send_select_info(select); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_info(media_player); | ||||
|   this->client_->send_media_player_info(media_player); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
| bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||
|   return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); | ||||
|   this->client_->send_alarm_control_panel_info(a_alarm_control_panel); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_EVENT | ||||
| bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); } | ||||
| bool ListEntitiesIterator::on_event(event::Event *event) { | ||||
|   this->client_->send_event_info(event); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
| bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); } | ||||
| bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { | ||||
|   this->client_->send_update_info(update); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_API | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -80,6 +80,7 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|   bool on_update(update::UpdateEntity *update) override; | ||||
| #endif | ||||
|   bool on_end() override; | ||||
|   bool completed() { return this->state_ == IteratorState::NONE; } | ||||
|  | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
| @@ -87,3 +88,4 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "subscribe_state.h" | ||||
| #ifdef USE_API | ||||
| #include "api_connection.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| @@ -84,3 +85,4 @@ InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(clie | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_API | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| #include "esphome/core/controller.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -76,9 +76,12 @@ class InitialStateIterator : public ComponentIterator { | ||||
| #ifdef USE_UPDATE | ||||
|   bool on_update(update::UpdateEntity *update) override; | ||||
| #endif | ||||
|   bool completed() { return this->state_ == IteratorState::NONE; } | ||||
|  | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| # Dummy integration to allow relying on AsyncTCP | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.const import ( | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| CODEOWNERS = ["@OttoWinter"] | ||||
|  | ||||
| @@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/esphome/AsyncTCP/blob/master/library.json | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "2.1.3") | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/esphome/ESPAsyncTCP | ||||
|         cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") | ||||
|   | ||||
| @@ -1,34 +1,34 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, spi | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_REACTIVE_POWER, | ||||
|     CONF_VOLTAGE, | ||||
|     CONF_CURRENT, | ||||
|     CONF_FORWARD_ACTIVE_ENERGY, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_ID, | ||||
|     CONF_LINE_FREQUENCY, | ||||
|     CONF_POWER, | ||||
|     CONF_POWER_FACTOR, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_FORWARD_ACTIVE_ENERGY, | ||||
|     CONF_REACTIVE_POWER, | ||||
|     CONF_REVERSE_ACTIVE_ENERGY, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_LIGHTBULB, | ||||
|     ICON_CURRENT_AC, | ||||
|     ICON_LIGHTBULB, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     STATE_CLASS_TOTAL_INCREASING, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_HERTZ, | ||||
|     UNIT_VOLT, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_WATT, | ||||
|     UNIT_VOLT_AMPS_REACTIVE, | ||||
|     UNIT_WATT, | ||||
|     UNIT_WATT_HOURS, | ||||
| ) | ||||
|  | ||||
| CONF_LINE_FREQUENCY = "line_frequency" | ||||
| CONF_METER_CONSTANT = "meter_constant" | ||||
| CONF_PL_CONST = "pl_const" | ||||
| CONF_GAIN_PGA = "gain_pga" | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| CODEOWNERS = ["@circuitsetup", "@descipher"] | ||||
|  | ||||
| atm90e32_ns = cg.esphome_ns.namespace("atm90e32") | ||||
|  | ||||
| CONF_ATM90E32_ID = "atm90e32_id" | ||||
|   | ||||
| @@ -132,10 +132,77 @@ void ATM90E32Component::update() { | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| void ATM90E32Component::restore_calibrations_() { | ||||
|   if (enable_offset_calibration_) { | ||||
|     this->pref_.load(&this->offset_phase_); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| void ATM90E32Component::run_offset_calibrations() { | ||||
|   // Run the calibrations and | ||||
|   // Setup voltage and current calibration offsets for PHASE A | ||||
|   this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); | ||||
|   this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); | ||||
|   this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_);  // C Current offset | ||||
|   // Setup voltage and current calibration offsets for PHASE B | ||||
|   this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); | ||||
|   this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); | ||||
|   this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_);  // C Current offset | ||||
|   // Setup voltage and current calibration offsets for PHASE C | ||||
|   this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); | ||||
|   this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); | ||||
|   this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_);  // C Current offset | ||||
|   this->pref_.save(&this->offset_phase_); | ||||
|   ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, | ||||
|            this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); | ||||
|   ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, | ||||
|            this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); | ||||
| } | ||||
|  | ||||
| void ATM90E32Component::clear_offset_calibrations() { | ||||
|   // Clear the calibrations and | ||||
|   this->offset_phase_[PHASEA].voltage_offset_ = 0; | ||||
|   this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEA].current_offset_ = 0; | ||||
|   this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_);  // C Current offset | ||||
|   this->offset_phase_[PHASEB].voltage_offset_ = 0; | ||||
|   this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEB].current_offset_ = 0; | ||||
|   this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_);  // C Current offset | ||||
|   this->offset_phase_[PHASEC].voltage_offset_ = 0; | ||||
|   this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_);  // C Voltage offset | ||||
|   this->offset_phase_[PHASEC].current_offset_ = 0; | ||||
|   this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_);  // C Current offset | ||||
|   this->pref_.save(&this->offset_phase_); | ||||
|   ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, | ||||
|            this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); | ||||
|   ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, | ||||
|            this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); | ||||
| } | ||||
|  | ||||
| void ATM90E32Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); | ||||
|   this->spi_setup(); | ||||
|  | ||||
|   if (this->enable_offset_calibration_) { | ||||
|     uint32_t hash = fnv1_hash(App.get_friendly_name()); | ||||
|     this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true); | ||||
|     this->restore_calibrations_(); | ||||
|   } | ||||
|   uint16_t mmode0 = 0x87;  // 3P4W 50Hz | ||||
|   if (line_freq_ == 60) { | ||||
|     mmode0 |= 1 << 12;  // sets 12th bit to 1, 60Hz | ||||
| @@ -167,27 +234,12 @@ void ATM90E32Component::setup() { | ||||
|   this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C);       // All Reactive Startup Power Threshold - 50% | ||||
|   this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE);       // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 | ||||
|   this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE);       // Each phase Reactive Phase Threshold - 10% | ||||
|   // Setup voltage and current calibration offsets for PHASE A | ||||
|   this->phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_);  // A Voltage offset | ||||
|   this->phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_);  // A Current offset | ||||
|   // Setup voltage and current gain for PHASE A | ||||
|   this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_);  // A Voltage rms gain | ||||
|   this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_);       // A line current gain | ||||
|   // Setup voltage and current calibration offsets for PHASE B | ||||
|   this->phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_);  // B Voltage offset | ||||
|   this->phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_);  // B Current offset | ||||
|   // Setup voltage and current gain for PHASE B | ||||
|   this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_);  // B Voltage rms gain | ||||
|   this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_);       // B line current gain | ||||
|   // Setup voltage and current calibration offsets for PHASE C | ||||
|   this->phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); | ||||
|   this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_);  // C Voltage offset | ||||
|   this->phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); | ||||
|   this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_);  // C Current offset | ||||
|   // Setup voltage and current gain for PHASE C | ||||
|   this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_);  // C Voltage rms gain | ||||
|   this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_);       // C line current gain | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "atm90e32_reg.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "atm90e32_reg.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace atm90e32 { | ||||
| @@ -20,7 +23,6 @@ class ATM90E32Component : public PollingComponent, | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void update() override; | ||||
|  | ||||
|   void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } | ||||
|   void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } | ||||
|   void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } | ||||
| @@ -48,9 +50,11 @@ class ATM90E32Component : public PollingComponent, | ||||
|   void set_line_freq(int freq) { line_freq_ = freq; } | ||||
|   void set_current_phases(int phases) { current_phases_ = phases; } | ||||
|   void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } | ||||
|   void run_offset_calibrations(); | ||||
|   void clear_offset_calibrations(); | ||||
|   void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } | ||||
|   uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); | ||||
|   uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); | ||||
|  | ||||
|   int32_t last_periodic_millis = millis(); | ||||
|  | ||||
|  protected: | ||||
| @@ -83,10 +87,11 @@ class ATM90E32Component : public PollingComponent, | ||||
|   float get_chip_temperature_(); | ||||
|   bool get_publish_interval_flag_() { return publish_interval_flag_; }; | ||||
|   void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; | ||||
|   void restore_calibrations_(); | ||||
|  | ||||
|   struct ATM90E32Phase { | ||||
|     uint16_t voltage_gain_{7305}; | ||||
|     uint16_t ct_gain_{27961}; | ||||
|     uint16_t voltage_gain_{0}; | ||||
|     uint16_t ct_gain_{0}; | ||||
|     uint16_t voltage_offset_{0}; | ||||
|     uint16_t current_offset_{0}; | ||||
|     float voltage_{0}; | ||||
| @@ -114,13 +119,21 @@ class ATM90E32Component : public PollingComponent, | ||||
|     uint32_t cumulative_reverse_active_energy_{0}; | ||||
|   } phase_[3]; | ||||
|  | ||||
|   struct Calibration { | ||||
|     uint16_t voltage_offset_{0}; | ||||
|     uint16_t current_offset_{0}; | ||||
|   } offset_phase_[3]; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
|  | ||||
|   sensor::Sensor *freq_sensor_{nullptr}; | ||||
|   sensor::Sensor *chip_temperature_sensor_{nullptr}; | ||||
|   uint16_t pga_gain_{0x15}; | ||||
|   int line_freq_{60}; | ||||
|   int current_phases_{3}; | ||||
|   bool publish_interval_flag_{true}; | ||||
|   bool publish_interval_flag_{false}; | ||||
|   bool peak_current_signed_{false}; | ||||
|   bool enable_offset_calibration_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace atm90e32 | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cinttypes> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace atm90e32 { | ||||
|  | ||||
|   | ||||
							
								
								
									
										43
									
								
								esphome/components/atm90e32/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/atm90e32/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import button | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE | ||||
|  | ||||
| from .. import atm90e32_ns | ||||
| from ..sensor import ATM90E32Component | ||||
|  | ||||
| CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" | ||||
| CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" | ||||
|  | ||||
| ATM90E32CalibrationButton = atm90e32_ns.class_( | ||||
|     "ATM90E32CalibrationButton", | ||||
|     button.Button, | ||||
| ) | ||||
| ATM90E32ClearCalibrationButton = atm90e32_ns.class_( | ||||
|     "ATM90E32ClearCalibrationButton", | ||||
|     button.Button, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), | ||||
|     cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( | ||||
|         ATM90E32CalibrationButton, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_SCALE, | ||||
|     ), | ||||
|     cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( | ||||
|         ATM90E32ClearCalibrationButton, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_CHIP, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     parent = await cg.get_variable(config[CONF_ID]) | ||||
|     if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): | ||||
|         b = await button.new_button(run_offset) | ||||
|         await cg.register_parented(b, parent) | ||||
|     if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): | ||||
|         b = await button.new_button(clear_offset) | ||||
|         await cg.register_parented(b, parent) | ||||
							
								
								
									
										20
									
								
								esphome/components/atm90e32/button/atm90e32_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/atm90e32/button/atm90e32_button.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| #include "atm90e32_button.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace atm90e32 { | ||||
|  | ||||
| static const char *const TAG = "atm90e32.button"; | ||||
|  | ||||
| void ATM90E32CalibrationButton::press_action() { | ||||
|   ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); | ||||
|   this->parent_->run_offset_calibrations(); | ||||
| } | ||||
|  | ||||
| void ATM90E32ClearCalibrationButton::press_action() { | ||||
|   ESP_LOGI(TAG, "Offset calibrations cleared."); | ||||
|   this->parent_->clear_offset_calibrations(); | ||||
| } | ||||
|  | ||||
| }  // namespace atm90e32 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										27
									
								
								esphome/components/atm90e32/button/atm90e32_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/atm90e32/button/atm90e32_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/atm90e32/atm90e32.h" | ||||
| #include "esphome/components/button/button.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace atm90e32 { | ||||
|  | ||||
| class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> { | ||||
|  public: | ||||
|   ATM90E32CalibrationButton() = default; | ||||
|  | ||||
|  protected: | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> { | ||||
|  public: | ||||
|   ATM90E32ClearCalibrationButton() = default; | ||||
|  | ||||
|  protected: | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace atm90e32 | ||||
| }  // namespace esphome | ||||
| @@ -1,21 +1,22 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, spi | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_REACTIVE_POWER, | ||||
|     CONF_VOLTAGE, | ||||
|     CONF_APPARENT_POWER, | ||||
|     CONF_CURRENT, | ||||
|     CONF_FORWARD_ACTIVE_ENERGY, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_ID, | ||||
|     CONF_LINE_FREQUENCY, | ||||
|     CONF_PHASE_A, | ||||
|     CONF_PHASE_ANGLE, | ||||
|     CONF_PHASE_B, | ||||
|     CONF_PHASE_C, | ||||
|     CONF_PHASE_ANGLE, | ||||
|     CONF_POWER, | ||||
|     CONF_POWER_FACTOR, | ||||
|     CONF_APPARENT_POWER, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_FORWARD_ACTIVE_ENERGY, | ||||
|     CONF_REACTIVE_POWER, | ||||
|     CONF_REVERSE_ACTIVE_ENERGY, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
|     DEVICE_CLASS_POWER, | ||||
| @@ -23,13 +24,13 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     ICON_LIGHTBULB, | ||||
|     ICON_CURRENT_AC, | ||||
|     ICON_LIGHTBULB, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     STATE_CLASS_TOTAL_INCREASING, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_DEGREES, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_DEGREES, | ||||
|     UNIT_HERTZ, | ||||
|     UNIT_VOLT, | ||||
|     UNIT_VOLT_AMPS_REACTIVE, | ||||
| @@ -37,7 +38,8 @@ from esphome.const import ( | ||||
|     UNIT_WATT_HOURS, | ||||
| ) | ||||
|  | ||||
| CONF_LINE_FREQUENCY = "line_frequency" | ||||
| from . import atm90e32_ns | ||||
|  | ||||
| CONF_CHIP_TEMPERATURE = "chip_temperature" | ||||
| CONF_GAIN_PGA = "gain_pga" | ||||
| CONF_CURRENT_PHASES = "current_phases" | ||||
| @@ -46,6 +48,7 @@ CONF_GAIN_CT = "gain_ct" | ||||
| CONF_HARMONIC_POWER = "harmonic_power" | ||||
| CONF_PEAK_CURRENT = "peak_current" | ||||
| CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" | ||||
| CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" | ||||
| UNIT_DEG = "degrees" | ||||
| LINE_FREQS = { | ||||
|     "50HZ": 50, | ||||
| @@ -61,7 +64,6 @@ PGA_GAINS = { | ||||
|     "4X": 0x2A, | ||||
| } | ||||
|  | ||||
| atm90e32_ns = cg.esphome_ns.namespace("atm90e32") | ||||
| ATM90E32Component = atm90e32_ns.class_( | ||||
|     "ATM90E32Component", cg.PollingComponent, spi.SPIDevice | ||||
| ) | ||||
| @@ -164,6 +166,7 @@ CONFIG_SCHEMA = ( | ||||
|             ), | ||||
|             cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), | ||||
|             cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| @@ -227,3 +230,4 @@ async def to_code(config): | ||||
|     cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) | ||||
|     cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) | ||||
|     cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) | ||||
|     cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) | ||||
|   | ||||
							
								
								
									
										121
									
								
								esphome/components/audio/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/audio/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| CODEOWNERS = ["@kahrendt"] | ||||
| audio_ns = cg.esphome_ns.namespace("audio") | ||||
|  | ||||
| AudioFile = audio_ns.struct("AudioFile") | ||||
| AudioFileType = audio_ns.enum("AudioFileType", is_class=True) | ||||
| AUDIO_FILE_TYPE_ENUM = { | ||||
|     "NONE": AudioFileType.NONE, | ||||
|     "WAV": AudioFileType.WAV, | ||||
|     "MP3": AudioFileType.MP3, | ||||
|     "FLAC": AudioFileType.FLAC, | ||||
| } | ||||
|  | ||||
|  | ||||
| CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" | ||||
| CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" | ||||
| CONF_MIN_CHANNELS = "min_channels" | ||||
| CONF_MAX_CHANNELS = "max_channels" | ||||
| CONF_MIN_SAMPLE_RATE = "min_sample_rate" | ||||
| CONF_MAX_SAMPLE_RATE = "max_sample_rate" | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema({}), | ||||
| ) | ||||
|  | ||||
| AUDIO_COMPONENT_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_BITS_PER_SAMPLE): cv.int_range(8, 32), | ||||
|         cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2), | ||||
|         cv.Optional(CONF_SAMPLE_RATE): cv.int_range(8000, 48000), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| _UNDEF = object() | ||||
|  | ||||
|  | ||||
| def set_stream_limits( | ||||
|     min_bits_per_sample: int = _UNDEF, | ||||
|     max_bits_per_sample: int = _UNDEF, | ||||
|     min_channels: int = _UNDEF, | ||||
|     max_channels: int = _UNDEF, | ||||
|     min_sample_rate: int = _UNDEF, | ||||
|     max_sample_rate: int = _UNDEF, | ||||
| ): | ||||
|     def set_limits_in_config(config): | ||||
|         if min_bits_per_sample is not _UNDEF: | ||||
|             config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample | ||||
|         if max_bits_per_sample is not _UNDEF: | ||||
|             config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample | ||||
|         if min_channels is not _UNDEF: | ||||
|             config[CONF_MIN_CHANNELS] = min_channels | ||||
|         if max_channels is not _UNDEF: | ||||
|             config[CONF_MAX_CHANNELS] = max_channels | ||||
|         if min_sample_rate is not _UNDEF: | ||||
|             config[CONF_MIN_SAMPLE_RATE] = min_sample_rate | ||||
|         if max_sample_rate is not _UNDEF: | ||||
|             config[CONF_MAX_SAMPLE_RATE] = max_sample_rate | ||||
|  | ||||
|     return set_limits_in_config | ||||
|  | ||||
|  | ||||
| def final_validate_audio_schema( | ||||
|     name: str, | ||||
|     *, | ||||
|     audio_device: str, | ||||
|     bits_per_sample: int, | ||||
|     channels: int, | ||||
|     sample_rate: int, | ||||
| ): | ||||
|     def validate_audio_compatiblity(audio_config): | ||||
|         audio_schema = {} | ||||
|  | ||||
|         try: | ||||
|             cv.int_range( | ||||
|                 min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), | ||||
|                 max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE), | ||||
|             )(bits_per_sample) | ||||
|         except cv.Invalid as exc: | ||||
|             raise cv.Invalid( | ||||
|                 f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}" | ||||
|             ) from exc | ||||
|  | ||||
|         try: | ||||
|             cv.int_range( | ||||
|                 min=audio_config.get(CONF_MIN_CHANNELS), | ||||
|                 max=audio_config.get(CONF_MAX_CHANNELS), | ||||
|             )(channels) | ||||
|         except cv.Invalid as exc: | ||||
|             raise cv.Invalid( | ||||
|                 f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}" | ||||
|             ) from exc | ||||
|  | ||||
|         try: | ||||
|             cv.int_range( | ||||
|                 min=audio_config.get(CONF_MIN_SAMPLE_RATE), | ||||
|                 max=audio_config.get(CONF_MAX_SAMPLE_RATE), | ||||
|             )(sample_rate) | ||||
|             return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config) | ||||
|         except cv.Invalid as exc: | ||||
|             raise cv.Invalid( | ||||
|                 f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}" | ||||
|             ) from exc | ||||
|  | ||||
|     return cv.Schema( | ||||
|         { | ||||
|             cv.Required(audio_device): fv.id_declaration_match_schema( | ||||
|                 validate_audio_compatiblity | ||||
|             ) | ||||
|         }, | ||||
|         extra=cv.ALLOW_EXTRA, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     cg.add_library("esphome/esp-audio-libs", "1.1.3") | ||||
							
								
								
									
										67
									
								
								esphome/components/audio/audio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								esphome/components/audio/audio.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| #include "audio.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| // Euclidean's algorithm for finding the greatest common divisor | ||||
| static uint32_t gcd(uint32_t a, uint32_t b) { | ||||
|   while (b != 0) { | ||||
|     uint32_t t = b; | ||||
|     b = a % b; | ||||
|     a = t; | ||||
|   } | ||||
|   return a; | ||||
| } | ||||
|  | ||||
| AudioStreamInfo::AudioStreamInfo(uint8_t bits_per_sample, uint8_t channels, uint32_t sample_rate) | ||||
|     : bits_per_sample_(bits_per_sample), channels_(channels), sample_rate_(sample_rate) { | ||||
|   this->ms_sample_rate_gcd_ = gcd(1000, this->sample_rate_); | ||||
|   this->bytes_per_sample_ = (this->bits_per_sample_ + 7) / 8; | ||||
| } | ||||
|  | ||||
| uint32_t AudioStreamInfo::frames_to_microseconds(uint32_t frames) const { | ||||
|   return (frames * 1000000 + (this->sample_rate_ >> 1)) / this->sample_rate_; | ||||
| } | ||||
|  | ||||
| uint32_t AudioStreamInfo::frames_to_milliseconds_with_remainder(uint32_t *total_frames) const { | ||||
|   uint32_t unprocessable_frames = *total_frames % (this->sample_rate_ / this->ms_sample_rate_gcd_); | ||||
|   uint32_t frames_for_ms_calculation = *total_frames - unprocessable_frames; | ||||
|  | ||||
|   uint32_t playback_ms = (frames_for_ms_calculation * 1000) / this->sample_rate_; | ||||
|   *total_frames = unprocessable_frames; | ||||
|   return playback_ms; | ||||
| } | ||||
|  | ||||
| bool AudioStreamInfo::operator==(const AudioStreamInfo &rhs) const { | ||||
|   return (this->bits_per_sample_ == rhs.get_bits_per_sample()) && (this->channels_ == rhs.get_channels()) && | ||||
|          (this->sample_rate_ == rhs.get_sample_rate()); | ||||
| } | ||||
|  | ||||
| const char *audio_file_type_to_string(AudioFileType file_type) { | ||||
|   switch (file_type) { | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|     case AudioFileType::FLAC: | ||||
|       return "FLAC"; | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|     case AudioFileType::MP3: | ||||
|       return "MP3"; | ||||
| #endif | ||||
|     case AudioFileType::WAV: | ||||
|       return "WAV"; | ||||
|     default: | ||||
|       return "unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, | ||||
|                          size_t samples_to_scale) { | ||||
|   // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. | ||||
|   for (int i = 0; i < samples_to_scale; i++) { | ||||
|     int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor; | ||||
|     output_buffer[i] = (int16_t) (acc >> 15); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
							
								
								
									
										139
									
								
								esphome/components/audio/audio.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								esphome/components/audio/audio.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #include <cstddef> | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| class AudioStreamInfo { | ||||
|   /* Class to respresent important parameters of the audio stream that also provides helper function to convert between | ||||
|    * various audio related units. | ||||
|    * | ||||
|    *  - An audio sample represents a unit of audio for one channel. | ||||
|    *  - A frame represents a unit of audio with a sample for every channel. | ||||
|    * | ||||
|    * In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames | ||||
|    * are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates; | ||||
|    * e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes | ||||
|    * into account the remainder rather than just ignoring any rounding. | ||||
|    */ | ||||
|  public: | ||||
|   AudioStreamInfo() | ||||
|       : AudioStreamInfo(16, 1, 16000){};  // Default values represent ESPHome's audio components historical values | ||||
|   AudioStreamInfo(uint8_t bits_per_sample, uint8_t channels, uint32_t sample_rate); | ||||
|  | ||||
|   uint8_t get_bits_per_sample() const { return this->bits_per_sample_; } | ||||
|   uint8_t get_channels() const { return this->channels_; } | ||||
|   uint32_t get_sample_rate() const { return this->sample_rate_; } | ||||
|  | ||||
|   /// @brief Convert bytes to duration in milliseconds. | ||||
|   /// @param bytes Number of bytes to convert | ||||
|   /// @return Duration in milliseconds that will store `bytes` bytes of audio. May round down for certain sample rates | ||||
|   ///         or values of `bytes`. | ||||
|   uint32_t bytes_to_ms(size_t bytes) const { | ||||
|     return bytes * 1000 / (this->sample_rate_ * this->bytes_per_sample_ * this->channels_); | ||||
|   } | ||||
|  | ||||
|   /// @brief Convert bytes to frames. | ||||
|   /// @param bytes Number of bytes to convert | ||||
|   /// @return Audio frames that will store `bytes` bytes. | ||||
|   uint32_t bytes_to_frames(size_t bytes) const { return (bytes / (this->bytes_per_sample_ * this->channels_)); } | ||||
|  | ||||
|   /// @brief Convert bytes to samples. | ||||
|   /// @param bytes Number of bytes to convert | ||||
|   /// @return Audio samples that will store `bytes` bytes. | ||||
|   uint32_t bytes_to_samples(size_t bytes) const { return (bytes / this->bytes_per_sample_); } | ||||
|  | ||||
|   /// @brief Converts frames to bytes. | ||||
|   /// @param frames Number of frames to convert. | ||||
|   /// @return Number of bytes that will store `frames` frames of audio. | ||||
|   size_t frames_to_bytes(uint32_t frames) const { return frames * this->bytes_per_sample_ * this->channels_; } | ||||
|  | ||||
|   /// @brief Converts samples to bytes. | ||||
|   /// @param samples Number of samples to convert. | ||||
|   /// @return Number of bytes that will store `samples` samples of audio. | ||||
|   size_t samples_to_bytes(uint32_t samples) const { return samples * this->bytes_per_sample_; } | ||||
|  | ||||
|   /// @brief Converts duration to frames. | ||||
|   /// @param ms Duration in milliseconds | ||||
|   /// @return Audio frames that will store `ms` milliseconds of audio.  May round down for certain sample rates. | ||||
|   uint32_t ms_to_frames(uint32_t ms) const { return (ms * this->sample_rate_) / 1000; } | ||||
|  | ||||
|   /// @brief Converts duration to samples. | ||||
|   /// @param ms Duration in milliseconds | ||||
|   /// @return Audio samples that will store `ms` milliseconds of audio.  May round down for certain sample rates. | ||||
|   uint32_t ms_to_samples(uint32_t ms) const { return (ms * this->channels_ * this->sample_rate_) / 1000; } | ||||
|  | ||||
|   /// @brief Converts duration to bytes. May round down for certain sample rates. | ||||
|   /// @param ms Duration in milliseconds | ||||
|   /// @return Bytes that will store `ms` milliseconds of audio.  May round down for certain sample rates. | ||||
|   size_t ms_to_bytes(uint32_t ms) const { | ||||
|     return (ms * this->bytes_per_sample_ * this->channels_ * this->sample_rate_) / 1000; | ||||
|   } | ||||
|  | ||||
|   /// @brief Computes the duration, in microseconds, the given amount of frames represents. | ||||
|   /// @param frames Number of audio frames | ||||
|   /// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding | ||||
|   ///         for certain sample rates. | ||||
|   uint32_t frames_to_microseconds(uint32_t frames) const; | ||||
|  | ||||
|   /// @brief Computes the duration, in milliseconds, the given amount of frames represents. Avoids | ||||
|   /// accumulating rounding errors by updating `frames` with the remainder after converting. | ||||
|   /// @param frames Pointer to uint32_t with the number of audio frames. Replaced with the remainder. | ||||
|   /// @return Duration in milliseconds `frames` represents. Always less than or equal to the actual value due to | ||||
|   ///         rounding. | ||||
|   uint32_t frames_to_milliseconds_with_remainder(uint32_t *frames) const; | ||||
|  | ||||
|   // Class comparison operators | ||||
|   bool operator==(const AudioStreamInfo &rhs) const; | ||||
|   bool operator!=(const AudioStreamInfo &rhs) const { return !operator==(rhs); } | ||||
|  | ||||
|  protected: | ||||
|   uint8_t bits_per_sample_; | ||||
|   uint8_t channels_; | ||||
|   uint32_t sample_rate_; | ||||
|  | ||||
|   // The greatest common divisor between 1000 ms = 1 second and the sample rate. Used to avoid accumulating error when | ||||
|   // converting from frames to duration. Computed at construction. | ||||
|   uint32_t ms_sample_rate_gcd_; | ||||
|  | ||||
|   // Conversion factor derived from the number of bits per sample. Assumes audio data is aligned to the byte. Computed | ||||
|   // at construction. | ||||
|   size_t bytes_per_sample_; | ||||
| }; | ||||
|  | ||||
| enum class AudioFileType : uint8_t { | ||||
|   NONE = 0, | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|   FLAC, | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|   MP3, | ||||
| #endif | ||||
|   WAV, | ||||
| }; | ||||
|  | ||||
| struct AudioFile { | ||||
|   const uint8_t *data; | ||||
|   size_t length; | ||||
|   AudioFileType file_type; | ||||
| }; | ||||
|  | ||||
| /// @brief Helper function to convert file type to a const char string | ||||
| /// @param file_type | ||||
| /// @return const char pointer to the readable file type | ||||
| const char *audio_file_type_to_string(AudioFileType file_type); | ||||
|  | ||||
| /// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer. | ||||
| /// @param audio_samples PCM int16 audio samples | ||||
| /// @param output_buffer Buffer to store the scaled samples | ||||
| /// @param scale_factor Q15 fixed point scaling factor | ||||
| /// @param samples_to_scale Number of samples to scale | ||||
| void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, | ||||
|                          size_t samples_to_scale); | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
							
								
								
									
										393
									
								
								esphome/components/audio/audio_decoder.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								esphome/components/audio/audio_decoder.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,393 @@ | ||||
| #include "audio_decoder.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| static const uint32_t DECODING_TIMEOUT_MS = 50;    // The decode function will yield after this duration | ||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20;  // Timeout for transferring audio data | ||||
|  | ||||
| static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10; | ||||
|  | ||||
| AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) { | ||||
|   this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); | ||||
|   this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); | ||||
| } | ||||
|  | ||||
| AudioDecoder::~AudioDecoder() { | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|   if (this->audio_file_type_ == AudioFileType::MP3) { | ||||
|     esp_audio_libs::helix_decoder::MP3FreeDecoder(this->mp3_decoder_); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) { | ||||
|   if (this->input_transfer_buffer_ != nullptr) { | ||||
|     this->input_transfer_buffer_->set_source(input_ring_buffer); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
|  | ||||
| esp_err_t AudioDecoder::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) { | ||||
|   if (this->output_transfer_buffer_ != nullptr) { | ||||
|     this->output_transfer_buffer_->set_sink(output_ring_buffer); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
| esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) { | ||||
|   if (this->output_transfer_buffer_ != nullptr) { | ||||
|     this->output_transfer_buffer_->set_sink(speaker); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { | ||||
|   if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { | ||||
|     return ESP_ERR_NO_MEM; | ||||
|   } | ||||
|  | ||||
|   this->audio_file_type_ = audio_file_type; | ||||
|  | ||||
|   this->potentially_failed_count_ = 0; | ||||
|   this->end_of_file_ = false; | ||||
|  | ||||
|   switch (this->audio_file_type_) { | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|     case AudioFileType::FLAC: | ||||
|       this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>(); | ||||
|       this->free_buffer_required_ = | ||||
|           this->output_transfer_buffer_->capacity();  // Adjusted and reallocated after reading the header | ||||
|       break; | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|     case AudioFileType::MP3: | ||||
|       this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder(); | ||||
|  | ||||
|       // MP3 always has 1152 samples per chunk | ||||
|       this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2;  // samples * size per sample * channels | ||||
|  | ||||
|       // Always reallocate the output transfer buffer to the smallest necessary size | ||||
|       this->output_transfer_buffer_->reallocate(this->free_buffer_required_); | ||||
|       break; | ||||
| #endif | ||||
|     case AudioFileType::WAV: | ||||
|       this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>(); | ||||
|       this->wav_decoder_->reset(); | ||||
|  | ||||
|       // Processing WAVs doesn't actually require a specific amount of buffer size, as it is already in PCM format. | ||||
|       // Thus, we don't reallocate to a minimum size. | ||||
|       this->free_buffer_required_ = 1024; | ||||
|       if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) { | ||||
|         this->output_transfer_buffer_->reallocate(this->free_buffer_required_); | ||||
|       } | ||||
|       break; | ||||
|     case AudioFileType::NONE: | ||||
|     default: | ||||
|       return ESP_ERR_NOT_SUPPORTED; | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { | ||||
|   if (stop_gracefully) { | ||||
|     if (this->output_transfer_buffer_->available() == 0) { | ||||
|       if (this->end_of_file_) { | ||||
|         // The file decoder indicates it reached the end of file | ||||
|         return AudioDecoderState::FINISHED; | ||||
|       } | ||||
|  | ||||
|       if (!this->input_transfer_buffer_->has_buffered_data()) { | ||||
|         // If all the internal buffers are empty, the decoding is done | ||||
|         return AudioDecoderState::FINISHED; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->potentially_failed_count_ > MAX_POTENTIALLY_FAILED_COUNT) { | ||||
|     if (stop_gracefully) { | ||||
|       // No more new data is going to come in, so decoding is done | ||||
|       return AudioDecoderState::FINISHED; | ||||
|     } | ||||
|     return AudioDecoderState::FAILED; | ||||
|   } | ||||
|  | ||||
|   FileDecoderState state = FileDecoderState::MORE_TO_PROCESS; | ||||
|  | ||||
|   uint32_t decoding_start = millis(); | ||||
|  | ||||
|   bool first_loop_iteration = true; | ||||
|  | ||||
|   size_t bytes_processed = 0; | ||||
|   size_t bytes_available_before_processing = 0; | ||||
|  | ||||
|   while (state == FileDecoderState::MORE_TO_PROCESS) { | ||||
|     // Transfer decoded out | ||||
|     if (!this->pause_output_) { | ||||
|       // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves | ||||
|       size_t bytes_written = | ||||
|           this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false); | ||||
|  | ||||
|       if (this->audio_stream_info_.has_value()) { | ||||
|         this->accumulated_frames_written_ += this->audio_stream_info_.value().bytes_to_frames(bytes_written); | ||||
|         this->playback_ms_ += | ||||
|             this->audio_stream_info_.value().frames_to_milliseconds_with_remainder(&this->accumulated_frames_written_); | ||||
|       } | ||||
|     } else { | ||||
|       // If paused, block to avoid wasting CPU resources | ||||
|       delay(READ_WRITE_TIMEOUT_MS); | ||||
|     } | ||||
|  | ||||
|     // Verify there is enough space to store more decoded audio and that the function hasn't been running too long | ||||
|     if ((this->output_transfer_buffer_->free() < this->free_buffer_required_) || | ||||
|         (millis() - decoding_start > DECODING_TIMEOUT_MS)) { | ||||
|       return AudioDecoderState::DECODING; | ||||
|     } | ||||
|  | ||||
|     // Decode more audio | ||||
|  | ||||
|     // Only shift data on the first loop iteration to avoid unnecessary, slow moves | ||||
|     size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), | ||||
|                                                                                 first_loop_iteration); | ||||
|  | ||||
|     if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { | ||||
|       // Less data is available than what was processed in last iteration, so don't attempt to decode. | ||||
|       // This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer | ||||
|       // will shift the remaining data to the start and copy more from the source the next time the decode function is | ||||
|       // called | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     bytes_available_before_processing = this->input_transfer_buffer_->available(); | ||||
|  | ||||
|     if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) { | ||||
|       // Failed to decode in last attempt and there is no new data | ||||
|  | ||||
|       if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { | ||||
|         // The input buffer is full. Since it previously failed on the exact same data, we can never recover | ||||
|         state = FileDecoderState::FAILED; | ||||
|       } else { | ||||
|         // Attempt to get more data next time | ||||
|         state = FileDecoderState::IDLE; | ||||
|       } | ||||
|     } else if (this->input_transfer_buffer_->available() == 0) { | ||||
|       // No data to decode, attempt to get more data next time | ||||
|       state = FileDecoderState::IDLE; | ||||
|     } else { | ||||
|       switch (this->audio_file_type_) { | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|         case AudioFileType::FLAC: | ||||
|           state = this->decode_flac_(); | ||||
|           break; | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|         case AudioFileType::MP3: | ||||
|           state = this->decode_mp3_(); | ||||
|           break; | ||||
| #endif | ||||
|         case AudioFileType::WAV: | ||||
|           state = this->decode_wav_(); | ||||
|           break; | ||||
|         case AudioFileType::NONE: | ||||
|         default: | ||||
|           state = FileDecoderState::IDLE; | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     first_loop_iteration = false; | ||||
|     bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available(); | ||||
|  | ||||
|     if (state == FileDecoderState::POTENTIALLY_FAILED) { | ||||
|       ++this->potentially_failed_count_; | ||||
|     } else if (state == FileDecoderState::END_OF_FILE) { | ||||
|       this->end_of_file_ = true; | ||||
|     } else if (state == FileDecoderState::FAILED) { | ||||
|       return AudioDecoderState::FAILED; | ||||
|     } else if (state == FileDecoderState::MORE_TO_PROCESS) { | ||||
|       this->potentially_failed_count_ = 0; | ||||
|     } | ||||
|   } | ||||
|   return AudioDecoderState::DECODING; | ||||
| } | ||||
|  | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
| FileDecoderState AudioDecoder::decode_flac_() { | ||||
|   if (!this->audio_stream_info_.has_value()) { | ||||
|     // Header hasn't been read | ||||
|     auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), | ||||
|                                                    this->input_transfer_buffer_->available()); | ||||
|  | ||||
|     if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { | ||||
|       return FileDecoderState::POTENTIALLY_FAILED; | ||||
|     } | ||||
|  | ||||
|     if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) { | ||||
|       // Couldn't read FLAC header | ||||
|       return FileDecoderState::FAILED; | ||||
|     } | ||||
|  | ||||
|     size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); | ||||
|     this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); | ||||
|  | ||||
|     // Reallocate the output transfer buffer to the smallest necessary size | ||||
|     this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); | ||||
|     if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { | ||||
|       // Couldn't reallocate output buffer | ||||
|       return FileDecoderState::FAILED; | ||||
|     } | ||||
|  | ||||
|     this->audio_stream_info_ = | ||||
|         audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(), | ||||
|                                this->flac_decoder_->get_sample_rate()); | ||||
|  | ||||
|     return FileDecoderState::MORE_TO_PROCESS; | ||||
|   } | ||||
|  | ||||
|   uint32_t output_samples = 0; | ||||
|   auto result = this->flac_decoder_->decode_frame( | ||||
|       this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), | ||||
|       reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples); | ||||
|  | ||||
|   if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { | ||||
|     // Not an issue, just needs more data that we'll get next time. | ||||
|     return FileDecoderState::POTENTIALLY_FAILED; | ||||
|   } | ||||
|  | ||||
|   size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); | ||||
|   this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); | ||||
|  | ||||
|   if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { | ||||
|     // Corrupted frame, don't retry with current buffer content, wait for new sync | ||||
|     return FileDecoderState::POTENTIALLY_FAILED; | ||||
|   } | ||||
|  | ||||
|   // We have successfully decoded some input data and have new output data | ||||
|   this->output_transfer_buffer_->increase_buffer_length( | ||||
|       this->audio_stream_info_.value().samples_to_bytes(output_samples)); | ||||
|  | ||||
|   if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) { | ||||
|     return FileDecoderState::END_OF_FILE; | ||||
|   } | ||||
|  | ||||
|   return FileDecoderState::MORE_TO_PROCESS; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
| FileDecoderState AudioDecoder::decode_mp3_() { | ||||
|   // Look for the next sync word | ||||
|   int buffer_length = (int) this->input_transfer_buffer_->available(); | ||||
|   int32_t offset = | ||||
|       esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length); | ||||
|  | ||||
|   if (offset < 0) { | ||||
|     // New data may have the sync word | ||||
|     this->input_transfer_buffer_->decrease_buffer_length(buffer_length); | ||||
|     return FileDecoderState::POTENTIALLY_FAILED; | ||||
|   } | ||||
|  | ||||
|   // Advance read pointer to match the offset for the syncword | ||||
|   this->input_transfer_buffer_->decrease_buffer_length(offset); | ||||
|   uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start(); | ||||
|  | ||||
|   buffer_length = (int) this->input_transfer_buffer_->available(); | ||||
|   int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, | ||||
|                                                      (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); | ||||
|  | ||||
|   size_t consumed = this->input_transfer_buffer_->available() - buffer_length; | ||||
|   this->input_transfer_buffer_->decrease_buffer_length(consumed); | ||||
|  | ||||
|   if (err) { | ||||
|     switch (err) { | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: | ||||
|         // Intentional fallthrough | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: | ||||
|         return FileDecoderState::FAILED; | ||||
|         break; | ||||
|       default: | ||||
|         // Most errors are recoverable by moving on to the next frame, so mark as potentailly failed | ||||
|         return FileDecoderState::POTENTIALLY_FAILED; | ||||
|         break; | ||||
|     } | ||||
|   } else { | ||||
|     esp_audio_libs::helix_decoder::MP3FrameInfo mp3_frame_info; | ||||
|     esp_audio_libs::helix_decoder::MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info); | ||||
|     if (mp3_frame_info.outputSamps > 0) { | ||||
|       int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8); | ||||
|       this->output_transfer_buffer_->increase_buffer_length(mp3_frame_info.outputSamps * bytes_per_sample); | ||||
|  | ||||
|       if (!this->audio_stream_info_.has_value()) { | ||||
|         this->audio_stream_info_ = | ||||
|             audio::AudioStreamInfo(mp3_frame_info.bitsPerSample, mp3_frame_info.nChans, mp3_frame_info.samprate); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return FileDecoderState::MORE_TO_PROCESS; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| FileDecoderState AudioDecoder::decode_wav_() { | ||||
|   if (!this->audio_stream_info_.has_value()) { | ||||
|     // Header hasn't been processed | ||||
|  | ||||
|     esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header( | ||||
|         this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); | ||||
|  | ||||
|     if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { | ||||
|       this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed()); | ||||
|  | ||||
|       this->audio_stream_info_ = audio::AudioStreamInfo( | ||||
|           this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); | ||||
|  | ||||
|       this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left(); | ||||
|       this->wav_has_known_end_ = (this->wav_bytes_left_ > 0); | ||||
|       return FileDecoderState::MORE_TO_PROCESS; | ||||
|     } else if (result == esp_audio_libs::wav_decoder::WAV_DECODER_WARNING_INCOMPLETE_DATA) { | ||||
|       // Available data didn't have the full header | ||||
|       return FileDecoderState::POTENTIALLY_FAILED; | ||||
|     } else { | ||||
|       return FileDecoderState::FAILED; | ||||
|     } | ||||
|   } else { | ||||
|     if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { | ||||
|       size_t bytes_to_copy = this->input_transfer_buffer_->available(); | ||||
|  | ||||
|       if (this->wav_has_known_end_) { | ||||
|         bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); | ||||
|       } | ||||
|  | ||||
|       bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); | ||||
|  | ||||
|       if (bytes_to_copy > 0) { | ||||
|         std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(), | ||||
|                     bytes_to_copy); | ||||
|         this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy); | ||||
|         this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); | ||||
|         if (this->wav_has_known_end_) { | ||||
|           this->wav_bytes_left_ -= bytes_to_copy; | ||||
|         } | ||||
|       } | ||||
|       return FileDecoderState::IDLE; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return FileDecoderState::END_OF_FILE; | ||||
| } | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										135
									
								
								esphome/components/audio/audio_decoder.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								esphome/components/audio/audio_decoder.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "audio.h" | ||||
| #include "audio_transfer_buffer.h" | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/ring_buffer.h" | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
| #include "esphome/components/speaker/speaker.h" | ||||
| #endif | ||||
|  | ||||
| #include "esp_err.h" | ||||
|  | ||||
| // esp-audio-libs | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
| #include <flac_decoder.h> | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
| #include <mp3_decoder.h> | ||||
| #endif | ||||
| #include <wav_decoder.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| enum class AudioDecoderState : uint8_t { | ||||
|   DECODING = 0,  // More data is available to decode | ||||
|   FINISHED,      // All file data has been decoded and transferred | ||||
|   FAILED,        // Encountered an error | ||||
| }; | ||||
|  | ||||
| // Only used within the AudioDecoder class; conveys the state of the particular file type decoder | ||||
| enum class FileDecoderState : uint8_t { | ||||
|   MORE_TO_PROCESS,     // Successsfully read a file chunk and more data is available to decode | ||||
|   IDLE,                // Not enough data to decode, waiting for more to be transferred | ||||
|   POTENTIALLY_FAILED,  // Decoder encountered a potentially recoverable error if more file data is available | ||||
|   FAILED,              // Decoder encoutnered an uncrecoverable error | ||||
|   END_OF_FILE,         // The specific file decoder knows its the end of the file | ||||
| }; | ||||
|  | ||||
| class AudioDecoder { | ||||
|   /* | ||||
|    * @brief Class that facilitates decoding an audio file. | ||||
|    * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker | ||||
|    * component). | ||||
|    * Supports wav, flac, and mp3 formats. | ||||
|    */ | ||||
|  public: | ||||
|   /// @brief Allocates the input and output transfer buffers | ||||
|   /// @param input_buffer_size Size of the input transfer buffer in bytes. | ||||
|   /// @param output_buffer_size Size of the output transfer buffer in bytes. | ||||
|   AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); | ||||
|  | ||||
|   /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) | ||||
|   ~AudioDecoder(); | ||||
|  | ||||
|   /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. | ||||
|   /// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer); | ||||
|  | ||||
|   /// @brief Adds a sink ring buffer for decoded audio. Takes ownership of the ring buffer in a shared_ptr. | ||||
|   /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer); | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
|   /// @brief Adds a sink speaker for decoded audio. | ||||
|   /// @param speaker pointer to speaker component | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_sink(speaker::Speaker *speaker); | ||||
| #endif | ||||
|  | ||||
|   /// @brief Sets up decoding the file | ||||
|   /// @param audio_file_type AudioFileType of the file | ||||
|   /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if | ||||
|   /// the format isn't supported. | ||||
|   esp_err_t start(AudioFileType audio_file_type); | ||||
|  | ||||
|   /// @brief Decodes audio from the ring buffer source and writes to the sink. | ||||
|   /// @param stop_gracefully If true, it indicates the file source is finished. The decoder will decode all the | ||||
|   /// reamining data and then finish. | ||||
|   /// @return AudioDecoderState | ||||
|   AudioDecoderState decode(bool stop_gracefully); | ||||
|  | ||||
|   /// @brief Gets the audio stream information, if it has been decoded from the files header | ||||
|   /// @return optional<AudioStreamInfo> with the audio information. If not available yet, returns no value. | ||||
|   const optional<audio::AudioStreamInfo> &get_audio_stream_info() const { return this->audio_stream_info_; } | ||||
|  | ||||
|   /// @brief Returns the duration of audio (in milliseconds) decoded and sent to the sink | ||||
|   /// @return Duration of decoded audio in milliseconds | ||||
|   uint32_t get_playback_ms() const { return this->playback_ms_; } | ||||
|  | ||||
|   /// @brief Pauses sending resampled audio to the sink. If paused, it will continue to process internal buffers. | ||||
|   /// @param pause_state If true, audio data is not sent to the sink. | ||||
|   void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; } | ||||
|  | ||||
|  protected: | ||||
|   std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_; | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|   FileDecoderState decode_flac_(); | ||||
|   std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_; | ||||
| #endif | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|   FileDecoderState decode_mp3_(); | ||||
|   esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; | ||||
| #endif | ||||
|   FileDecoderState decode_wav_(); | ||||
|  | ||||
|   std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_; | ||||
|   std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_; | ||||
|  | ||||
|   AudioFileType audio_file_type_{AudioFileType::NONE}; | ||||
|   optional<AudioStreamInfo> audio_stream_info_{}; | ||||
|  | ||||
|   size_t free_buffer_required_{0}; | ||||
|   size_t wav_bytes_left_{0}; | ||||
|  | ||||
|   uint32_t potentially_failed_count_{0}; | ||||
|   bool end_of_file_{false}; | ||||
|   bool wav_has_known_end_{false}; | ||||
|  | ||||
|   bool pause_output_{false}; | ||||
|  | ||||
|   uint32_t accumulated_frames_written_{0}; | ||||
|   uint32_t playback_ms_{0}; | ||||
| }; | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										308
									
								
								esphome/components/audio/audio_reader.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								esphome/components/audio/audio_reader.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | ||||
| #include "audio_reader.h" | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||
| #include "esp_crt_bundle.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | ||||
|  | ||||
| static const uint32_t CONNECTION_TIMEOUT_MS = 5000; | ||||
|  | ||||
| // The number of times the http read times out with no data before throwing an error | ||||
| static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; | ||||
|  | ||||
| static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; | ||||
|  | ||||
| static const uint8_t MAX_REDIRECTION = 5; | ||||
|  | ||||
| // Some common HTTP status codes - borrowed from http_request component accessed 20241224 | ||||
| enum HttpStatus { | ||||
|   HTTP_STATUS_OK = 200, | ||||
|   HTTP_STATUS_NO_CONTENT = 204, | ||||
|   HTTP_STATUS_PARTIAL_CONTENT = 206, | ||||
|  | ||||
|   /* 3xx - Redirection */ | ||||
|   HTTP_STATUS_MULTIPLE_CHOICES = 300, | ||||
|   HTTP_STATUS_MOVED_PERMANENTLY = 301, | ||||
|   HTTP_STATUS_FOUND = 302, | ||||
|   HTTP_STATUS_SEE_OTHER = 303, | ||||
|   HTTP_STATUS_NOT_MODIFIED = 304, | ||||
|   HTTP_STATUS_TEMPORARY_REDIRECT = 307, | ||||
|   HTTP_STATUS_PERMANENT_REDIRECT = 308, | ||||
|  | ||||
|   /* 4XX - CLIENT ERROR */ | ||||
|   HTTP_STATUS_BAD_REQUEST = 400, | ||||
|   HTTP_STATUS_UNAUTHORIZED = 401, | ||||
|   HTTP_STATUS_FORBIDDEN = 403, | ||||
|   HTTP_STATUS_NOT_FOUND = 404, | ||||
|   HTTP_STATUS_METHOD_NOT_ALLOWED = 405, | ||||
|   HTTP_STATUS_NOT_ACCEPTABLE = 406, | ||||
|   HTTP_STATUS_LENGTH_REQUIRED = 411, | ||||
|  | ||||
|   /* 5xx - Server Error */ | ||||
|   HTTP_STATUS_INTERNAL_ERROR = 500 | ||||
| }; | ||||
|  | ||||
| AudioReader::~AudioReader() { this->cleanup_connection_(); } | ||||
|  | ||||
| esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) { | ||||
|   if (current_audio_file_ != nullptr) { | ||||
|     // A transfer buffer isn't ncessary for a local file | ||||
|     this->file_ring_buffer_ = output_ring_buffer.lock(); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|  | ||||
|   if (this->output_transfer_buffer_ != nullptr) { | ||||
|     this->output_transfer_buffer_->set_sink(output_ring_buffer); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|  | ||||
|   return ESP_ERR_INVALID_STATE; | ||||
| } | ||||
|  | ||||
| esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) { | ||||
|   file_type = AudioFileType::NONE; | ||||
|  | ||||
|   this->current_audio_file_ = audio_file; | ||||
|  | ||||
|   this->file_current_ = audio_file->data; | ||||
|   file_type = audio_file->file_type; | ||||
|  | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | ||||
|   file_type = AudioFileType::NONE; | ||||
|  | ||||
|   this->cleanup_connection_(); | ||||
|  | ||||
|   if (uri.empty()) { | ||||
|     return ESP_ERR_INVALID_ARG; | ||||
|   } | ||||
|  | ||||
|   esp_http_client_config_t client_config = {}; | ||||
|  | ||||
|   client_config.url = uri.c_str(); | ||||
|   client_config.cert_pem = nullptr; | ||||
|   client_config.disable_auto_redirect = false; | ||||
|   client_config.max_redirection_count = 10; | ||||
|   client_config.event_handler = http_event_handler; | ||||
|   client_config.user_data = this; | ||||
|   client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; | ||||
|   client_config.keep_alive_enable = true; | ||||
|   client_config.timeout_ms = CONNECTION_TIMEOUT_MS;  // Shouldn't trigger watchdog resets if caller runs in a task | ||||
|  | ||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||
|   if (uri.find("https:") != std::string::npos) { | ||||
|     client_config.crt_bundle_attach = esp_crt_bundle_attach; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   this->client_ = esp_http_client_init(&client_config); | ||||
|  | ||||
|   if (this->client_ == nullptr) { | ||||
|     return ESP_FAIL; | ||||
|   } | ||||
|  | ||||
|   esp_err_t err = esp_http_client_open(this->client_, 0); | ||||
|  | ||||
|   if (err != ESP_OK) { | ||||
|     this->cleanup_connection_(); | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   int64_t header_length = esp_http_client_fetch_headers(this->client_); | ||||
|   if (header_length < 0) { | ||||
|     this->cleanup_connection_(); | ||||
|     return ESP_FAIL; | ||||
|   } | ||||
|  | ||||
|   int status_code = esp_http_client_get_status_code(this->client_); | ||||
|  | ||||
|   if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) { | ||||
|     this->cleanup_connection_(); | ||||
|     return ESP_FAIL; | ||||
|   } | ||||
|  | ||||
|   ssize_t redirect_count = 0; | ||||
|  | ||||
|   while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { | ||||
|     err = esp_http_client_open(this->client_, 0); | ||||
|     if (err != ESP_OK) { | ||||
|       this->cleanup_connection_(); | ||||
|       return ESP_FAIL; | ||||
|     } | ||||
|  | ||||
|     header_length = esp_http_client_fetch_headers(this->client_); | ||||
|     if (header_length < 0) { | ||||
|       this->cleanup_connection_(); | ||||
|       return ESP_FAIL; | ||||
|     } | ||||
|  | ||||
|     status_code = esp_http_client_get_status_code(this->client_); | ||||
|  | ||||
|     if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) { | ||||
|       this->cleanup_connection_(); | ||||
|       return ESP_FAIL; | ||||
|     } | ||||
|  | ||||
|     ++redirect_count; | ||||
|   } | ||||
|  | ||||
|   if (this->audio_file_type_ == AudioFileType::NONE) { | ||||
|     // Failed to determine the file type from the header, fallback to using the url | ||||
|     char url[500]; | ||||
|     err = esp_http_client_get_url(this->client_, url, 500); | ||||
|     if (err != ESP_OK) { | ||||
|       this->cleanup_connection_(); | ||||
|       return err; | ||||
|     } | ||||
|  | ||||
|     std::string url_string = str_lower_case(url); | ||||
|  | ||||
|     if (str_endswith(url_string, ".wav")) { | ||||
|       file_type = AudioFileType::WAV; | ||||
|     } | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|     else if (str_endswith(url_string, ".mp3")) { | ||||
|       file_type = AudioFileType::MP3; | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|     else if (str_endswith(url_string, ".flac")) { | ||||
|       file_type = AudioFileType::FLAC; | ||||
|     } | ||||
| #endif | ||||
|     else { | ||||
|       file_type = AudioFileType::NONE; | ||||
|       this->cleanup_connection_(); | ||||
|       return ESP_ERR_NOT_SUPPORTED; | ||||
|     } | ||||
|   } else { | ||||
|     file_type = this->audio_file_type_; | ||||
|   } | ||||
|  | ||||
|   this->last_data_read_ms_ = millis(); | ||||
|  | ||||
|   this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(this->buffer_size_); | ||||
|   if (this->output_transfer_buffer_ == nullptr) { | ||||
|     return ESP_ERR_NO_MEM; | ||||
|   } | ||||
|  | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| AudioReaderState AudioReader::read() { | ||||
|   if (this->client_ != nullptr) { | ||||
|     return this->http_read_(); | ||||
|   } else if (this->current_audio_file_ != nullptr) { | ||||
|     return this->file_read_(); | ||||
|   } | ||||
|  | ||||
|   return AudioReaderState::FAILED; | ||||
| } | ||||
|  | ||||
| AudioFileType AudioReader::get_audio_type(const char *content_type) { | ||||
| #ifdef USE_AUDIO_MP3_SUPPORT | ||||
|   if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || | ||||
|       strcasecmp(content_type, "audio/mpeg") == 0) { | ||||
|     return AudioFileType::MP3; | ||||
|   } | ||||
| #endif | ||||
|   if (strcasecmp(content_type, "audio/wav") == 0) { | ||||
|     return AudioFileType::WAV; | ||||
|   } | ||||
| #ifdef USE_AUDIO_FLAC_SUPPORT | ||||
|   if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { | ||||
|     return AudioFileType::FLAC; | ||||
|   } | ||||
| #endif | ||||
|   return AudioFileType::NONE; | ||||
| } | ||||
|  | ||||
| esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { | ||||
|   // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224 | ||||
|   AudioReader *this_reader = (AudioReader *) evt->user_data; | ||||
|  | ||||
|   switch (evt->event_id) { | ||||
|     case HTTP_EVENT_ON_HEADER: | ||||
|       if (strcasecmp(evt->header_key, "Content-Type") == 0) { | ||||
|         this_reader->audio_file_type_ = get_audio_type(evt->header_value); | ||||
|       } | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| AudioReaderState AudioReader::file_read_() { | ||||
|   size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data); | ||||
|   if (remaining_bytes > 0) { | ||||
|     size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes, | ||||
|                                                                               pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); | ||||
|     this->file_current_ += bytes_written; | ||||
|  | ||||
|     return AudioReaderState::READING; | ||||
|   } | ||||
|  | ||||
|   return AudioReaderState::FINISHED; | ||||
| } | ||||
|  | ||||
| AudioReaderState AudioReader::http_read_() { | ||||
|   this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false); | ||||
|  | ||||
|   if (esp_http_client_is_complete_data_received(this->client_)) { | ||||
|     if (this->output_transfer_buffer_->available() == 0) { | ||||
|       this->cleanup_connection_(); | ||||
|       return AudioReaderState::FINISHED; | ||||
|     } | ||||
|   } else if (this->output_transfer_buffer_->free() > 0) { | ||||
|     size_t bytes_to_read = this->output_transfer_buffer_->free(); | ||||
|     int received_len = | ||||
|         esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); | ||||
|  | ||||
|     if (received_len > 0) { | ||||
|       this->output_transfer_buffer_->increase_buffer_length(received_len); | ||||
|       this->last_data_read_ms_ = millis(); | ||||
|     } else if (received_len < 0) { | ||||
|       // HTTP read error | ||||
|       this->cleanup_connection_(); | ||||
|       return AudioReaderState::FAILED; | ||||
|     } else { | ||||
|       if (bytes_to_read > 0) { | ||||
|         // Read timed out | ||||
|         if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { | ||||
|           this->cleanup_connection_(); | ||||
|           return AudioReaderState::FAILED; | ||||
|         } | ||||
|  | ||||
|         delay(READ_WRITE_TIMEOUT_MS); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return AudioReaderState::READING; | ||||
| } | ||||
|  | ||||
| void AudioReader::cleanup_connection_() { | ||||
|   if (this->client_ != nullptr) { | ||||
|     esp_http_client_close(this->client_); | ||||
|     esp_http_client_cleanup(this->client_); | ||||
|     this->client_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										85
									
								
								esphome/components/audio/audio_reader.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								esphome/components/audio/audio_reader.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
|  | ||||
| #include "audio.h" | ||||
| #include "audio_transfer_buffer.h" | ||||
|  | ||||
| #include "esphome/core/ring_buffer.h" | ||||
|  | ||||
| #include "esp_err.h" | ||||
|  | ||||
| #include <esp_http_client.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| enum class AudioReaderState : uint8_t { | ||||
|   READING = 0,  // More data is available to read | ||||
|   FINISHED,     // All data has been read and transferred | ||||
|   FAILED,       // Encountered an error | ||||
| }; | ||||
|  | ||||
| class AudioReader { | ||||
|   /* | ||||
|    * @brief Class that facilitates reading a raw audio file. | ||||
|    * Files can be read from flash (stored in a AudioFile struct) or from an http source. | ||||
|    * The file data is sent to a ring buffer sink. | ||||
|    */ | ||||
|  public: | ||||
|   /// @brief Constructs an AudioReader object. | ||||
|   /// The transfer buffer isn't allocated here, but only if necessary (an http source) in the start function. | ||||
|   /// @param buffer_size Transfer buffer size in bytes. | ||||
|   AudioReader(size_t buffer_size) : buffer_size_(buffer_size) {} | ||||
|   ~AudioReader(); | ||||
|  | ||||
|   /// @brief Adds a sink ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr | ||||
|   /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership | ||||
|   /// @return  ESP_OK if successful, ESP_ERR_INVALID_STATE otherwise | ||||
|   esp_err_t add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer); | ||||
|  | ||||
|   /// @brief Starts reading an audio file from an http source. The transfer buffer is allocated here. | ||||
|   /// @param uri Web url to the http file. | ||||
|   /// @param file_type AudioFileType variable passed-by-reference indicating the type of file being read. | ||||
|   /// @return ESP_OK if successful, an ESP_ERR* code otherwise. | ||||
|   esp_err_t start(const std::string &uri, AudioFileType &file_type); | ||||
|  | ||||
|   /// @brief Starts reading an audio file from flash. No transfer buffer is allocated. | ||||
|   /// @param audio_file AudioFile struct containing the file. | ||||
|   /// @param file_type AudioFileType variable passed-by-reference indicating the type of file being read. | ||||
|   /// @return ESP_OK | ||||
|   esp_err_t start(AudioFile *audio_file, AudioFileType &file_type); | ||||
|  | ||||
|   /// @brief Reads new file data from the source and sends to the ring buffer sink. | ||||
|   /// @return AudioReaderState | ||||
|   AudioReaderState read(); | ||||
|  | ||||
|  protected: | ||||
|   /// @brief Monitors the http client events to attempt determining the file type from the Content-Type header | ||||
|   static esp_err_t http_event_handler(esp_http_client_event_t *evt); | ||||
|  | ||||
|   /// @brief Determines the audio file type from the http header's Content-Type key | ||||
|   /// @param content_type string with the Content-Type key | ||||
|   /// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE. | ||||
|   static AudioFileType get_audio_type(const char *content_type); | ||||
|  | ||||
|   AudioReaderState file_read_(); | ||||
|   AudioReaderState http_read_(); | ||||
|  | ||||
|   std::shared_ptr<RingBuffer> file_ring_buffer_; | ||||
|   std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_; | ||||
|   void cleanup_connection_(); | ||||
|  | ||||
|   size_t buffer_size_; | ||||
|   uint32_t last_data_read_ms_; | ||||
|  | ||||
|   esp_http_client_handle_t client_{nullptr}; | ||||
|  | ||||
|   AudioFile *current_audio_file_{nullptr}; | ||||
|   AudioFileType audio_file_type_{AudioFileType::NONE}; | ||||
|   const uint8_t *file_current_{nullptr}; | ||||
| }; | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										161
									
								
								esphome/components/audio/audio_resampler.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								esphome/components/audio/audio_resampler.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| #include "audio_resampler.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | ||||
|  | ||||
| AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_size) | ||||
|     : input_buffer_size_(input_buffer_size), output_buffer_size_(output_buffer_size) { | ||||
|   this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); | ||||
|   this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); | ||||
| } | ||||
|  | ||||
| esp_err_t AudioResampler::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) { | ||||
|   if (this->input_transfer_buffer_ != nullptr) { | ||||
|     this->input_transfer_buffer_->set_source(input_ring_buffer); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
|  | ||||
| esp_err_t AudioResampler::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) { | ||||
|   if (this->output_transfer_buffer_ != nullptr) { | ||||
|     this->output_transfer_buffer_->set_sink(output_ring_buffer); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
| esp_err_t AudioResampler::add_sink(speaker::Speaker *speaker) { | ||||
|   if (this->output_transfer_buffer_ != nullptr) { | ||||
|     this->output_transfer_buffer_->set_sink(speaker); | ||||
|     return ESP_OK; | ||||
|   } | ||||
|   return ESP_ERR_NO_MEM; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamInfo &output_stream_info, | ||||
|                                 uint16_t number_of_taps, uint16_t number_of_filters) { | ||||
|   this->input_stream_info_ = input_stream_info; | ||||
|   this->output_stream_info_ = output_stream_info; | ||||
|  | ||||
|   if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { | ||||
|     return ESP_ERR_NO_MEM; | ||||
|   } | ||||
|  | ||||
|   if ((input_stream_info.get_bits_per_sample() > 32) || (output_stream_info.get_bits_per_sample() > 32) || | ||||
|       (input_stream_info_.get_channels() != output_stream_info.get_channels())) { | ||||
|     return ESP_ERR_NOT_SUPPORTED; | ||||
|   } | ||||
|  | ||||
|   if ((input_stream_info.get_sample_rate() != output_stream_info.get_sample_rate()) || | ||||
|       (input_stream_info.get_bits_per_sample() != output_stream_info.get_bits_per_sample())) { | ||||
|     this->resampler_ = make_unique<esp_audio_libs::resampler::Resampler>( | ||||
|         input_stream_info.bytes_to_samples(this->input_buffer_size_), | ||||
|         output_stream_info.bytes_to_samples(this->output_buffer_size_)); | ||||
|  | ||||
|     // Use cascaded biquad filters when downsampling to avoid aliasing | ||||
|     bool use_pre_filter = output_stream_info.get_sample_rate() < input_stream_info.get_sample_rate(); | ||||
|  | ||||
|     esp_audio_libs::resampler::ResamplerConfiguration resample_config = { | ||||
|         .source_sample_rate = static_cast<float>(input_stream_info.get_sample_rate()), | ||||
|         .target_sample_rate = static_cast<float>(output_stream_info.get_sample_rate()), | ||||
|         .source_bits_per_sample = input_stream_info.get_bits_per_sample(), | ||||
|         .target_bits_per_sample = output_stream_info.get_bits_per_sample(), | ||||
|         .channels = input_stream_info_.get_channels(), | ||||
|         .use_pre_or_post_filter = use_pre_filter, | ||||
|         .subsample_interpolate = false,  // Doubles the CPU load. Using more filters is a better alternative | ||||
|         .number_of_taps = number_of_taps, | ||||
|         .number_of_filters = number_of_filters, | ||||
|     }; | ||||
|  | ||||
|     if (!this->resampler_->initialize(resample_config)) { | ||||
|       // Failed to allocate the resampler's internal buffers | ||||
|       return ESP_ERR_NO_MEM; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_differential) { | ||||
|   if (stop_gracefully) { | ||||
|     if (!this->input_transfer_buffer_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) { | ||||
|       return AudioResamplerState::FINISHED; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!this->pause_output_) { | ||||
|     // Move audio data to the sink without shifting the data in the output transfer buffer to avoid unnecessary, slow | ||||
|     // data moves | ||||
|     this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false); | ||||
|   } else { | ||||
|     // If paused, block to avoid wasting CPU resources | ||||
|     delay(READ_WRITE_TIMEOUT_MS); | ||||
|   } | ||||
|  | ||||
|   this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); | ||||
|  | ||||
|   if (this->input_transfer_buffer_->available() == 0) { | ||||
|     // No samples available to process | ||||
|     return AudioResamplerState::RESAMPLING; | ||||
|   } | ||||
|  | ||||
|   const size_t bytes_free = this->output_transfer_buffer_->free(); | ||||
|   const uint32_t frames_free = this->output_stream_info_.bytes_to_frames(bytes_free); | ||||
|  | ||||
|   const size_t bytes_available = this->input_transfer_buffer_->available(); | ||||
|   const uint32_t frames_available = this->input_stream_info_.bytes_to_frames(bytes_available); | ||||
|  | ||||
|   if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) || | ||||
|       (this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) { | ||||
|     // Adjust gain by -3 dB to avoid clipping due to the resampling process | ||||
|     esp_audio_libs::resampler::ResamplerResults results = | ||||
|         this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(), | ||||
|                                    this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3); | ||||
|  | ||||
|     this->input_transfer_buffer_->decrease_buffer_length(this->input_stream_info_.frames_to_bytes(results.frames_used)); | ||||
|     this->output_transfer_buffer_->increase_buffer_length( | ||||
|         this->output_stream_info_.frames_to_bytes(results.frames_generated)); | ||||
|  | ||||
|     // Resampling causes slight differences in the durations used versus generated. Computes the difference in | ||||
|     // millisconds. The callback function passing the played audio duration uses the difference to convert from output | ||||
|     // duration to input duration. | ||||
|     this->accumulated_frames_used_ += results.frames_used; | ||||
|     this->accumulated_frames_generated_ += results.frames_generated; | ||||
|  | ||||
|     const int32_t used_ms = | ||||
|         this->input_stream_info_.frames_to_milliseconds_with_remainder(&this->accumulated_frames_used_); | ||||
|     const int32_t generated_ms = | ||||
|         this->output_stream_info_.frames_to_milliseconds_with_remainder(&this->accumulated_frames_generated_); | ||||
|  | ||||
|     *ms_differential = used_ms - generated_ms; | ||||
|  | ||||
|   } else { | ||||
|     // No resampling required, copy samples directly to the output transfer buffer | ||||
|     *ms_differential = 0; | ||||
|  | ||||
|     const size_t bytes_to_transfer = std::min(this->output_stream_info_.frames_to_bytes(frames_free), | ||||
|                                               this->input_stream_info_.frames_to_bytes(frames_available)); | ||||
|  | ||||
|     std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(), | ||||
|                 (void *) this->input_transfer_buffer_->get_buffer_start(), bytes_to_transfer); | ||||
|  | ||||
|     this->input_transfer_buffer_->decrease_buffer_length(bytes_to_transfer); | ||||
|     this->output_transfer_buffer_->increase_buffer_length(bytes_to_transfer); | ||||
|   } | ||||
|  | ||||
|   return AudioResamplerState::RESAMPLING; | ||||
| } | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										101
									
								
								esphome/components/audio/audio_resampler.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								esphome/components/audio/audio_resampler.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "audio.h" | ||||
| #include "audio_transfer_buffer.h" | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/ring_buffer.h" | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
| #include "esphome/components/speaker/speaker.h" | ||||
| #endif | ||||
|  | ||||
| #include "esp_err.h" | ||||
|  | ||||
| #include <resampler.h>  // esp-audio-libs | ||||
|  | ||||
| namespace esphome { | ||||
| namespace audio { | ||||
|  | ||||
| enum class AudioResamplerState : uint8_t { | ||||
|   RESAMPLING,  // More data is available to resample | ||||
|   FINISHED,    // All file data has been resampled and transferred | ||||
|   FAILED,      // Unused state included for consistency among Audio classes | ||||
| }; | ||||
|  | ||||
| class AudioResampler { | ||||
|   /* | ||||
|    * @brief Class that facilitates resampling audio. | ||||
|    * The audio data is read from a ring buffer source, resampled, and sent to an audio sink (ring buffer or speaker | ||||
|    * component). Also supports converting bits per sample. | ||||
|    */ | ||||
|  public: | ||||
|   /// @brief Allocates the input and output transfer buffers | ||||
|   /// @param input_buffer_size Size of the input transfer buffer in bytes. | ||||
|   /// @param output_buffer_size Size of the output transfer buffer in bytes. | ||||
|   AudioResampler(size_t input_buffer_size, size_t output_buffer_size); | ||||
|  | ||||
|   /// @brief Adds a source ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr. | ||||
|   /// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer); | ||||
|  | ||||
|   /// @brief Adds a sink ring buffer for resampled audio. Takes ownership of the ring buffer in a shared_ptr. | ||||
|   /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer); | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
|   /// @brief Adds a sink speaker for decoded audio. | ||||
|   /// @param speaker pointer to speaker component | ||||
|   /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated | ||||
|   esp_err_t add_sink(speaker::Speaker *speaker); | ||||
| #endif | ||||
|  | ||||
|   /// @brief Sets up the class to resample. | ||||
|   /// @param input_stream_info The incoming sample rate, bits per sample, and number of channels | ||||
|   /// @param output_stream_info The desired outgoing sample rate, bits per sample, and number of channels | ||||
|   /// @param number_of_taps Number of taps per FIR filter | ||||
|   /// @param number_of_filters Number of FIR filters | ||||
|   /// @return ESP_OK if it is able to convert the incoming stream, | ||||
|   ///         ESP_ERR_NO_MEM if the transfer buffers failed to allocate, | ||||
|   ///         ESP_ERR_NOT_SUPPORTED if the stream can't be converted. | ||||
|   esp_err_t start(AudioStreamInfo &input_stream_info, AudioStreamInfo &output_stream_info, uint16_t number_of_taps, | ||||
|                   uint16_t number_of_filters); | ||||
|  | ||||
|   /// @brief Resamples audio from the ring buffer source and writes to the sink. | ||||
|   /// @param stop_gracefully If true, it indicates the file decoder is finished. The resampler will resample all the | ||||
|   ///                        remaining audio and then finish. | ||||
|   /// @param ms_differential Pointer to a (int32_t) variable that will store the difference, in milliseconds, between | ||||
|   ///                        the duration of input audio used and the duration of output audio generated. | ||||
|   /// @return AudioResamplerState | ||||
|   AudioResamplerState resample(bool stop_gracefully, int32_t *ms_differential); | ||||
|  | ||||
|   /// @brief Pauses sending resampled audio to the sink. If paused, it will continue to process internal buffers. | ||||
|   /// @param pause_state If true, audio data is not sent to the sink. | ||||
|   void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; } | ||||
|  | ||||
|  protected: | ||||
|   std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_; | ||||
|   std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_; | ||||
|  | ||||
|   size_t input_buffer_size_; | ||||
|   size_t output_buffer_size_; | ||||
|  | ||||
|   uint32_t accumulated_frames_used_{0}; | ||||
|   uint32_t accumulated_frames_generated_{0}; | ||||
|  | ||||
|   bool pause_output_{false}; | ||||
|  | ||||
|   AudioStreamInfo input_stream_info_; | ||||
|   AudioStreamInfo output_stream_info_; | ||||
|  | ||||
|   std::unique_ptr<esp_audio_libs::resampler::Resampler> resampler_; | ||||
| }; | ||||
|  | ||||
| }  // namespace audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user