mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-01 07:31:51 +00:00 
			
		
		
		
	Compare commits
	
		
			556 Commits
		
	
	
		
			2021.12.0b
			...
			2022.6.0b3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5c7c0834c0 | ||
|  | f3a25de11d | ||
|  | 041bef8bcd | ||
|  | 6e83790308 | ||
|  | d2d4eb4eae | ||
|  | 5942a3898c | ||
|  | 93421f0fa7 | ||
|  | 6cb5cd48c2 | ||
|  | 746fd1122f | ||
|  | 9663760ec5 | ||
|  | a3d73d1e23 | ||
|  | d63e14a4b6 | ||
|  | 03944e6cd8 | ||
|  | 0d1028be2e | ||
|  | 6a85259e4d | ||
|  | ebca936b7e | ||
|  | 31c4551890 | ||
|  | dd470d4197 | ||
|  | 612822490b | ||
|  | f8969605e8 | ||
|  | dd24ffa24e | ||
|  | d0dda48932 | ||
|  | 6349b5f654 | ||
|  | a6ff02a3cf | ||
|  | 4f57bf786b | ||
|  | 6221f6d47d | ||
|  | a922efeafa | ||
|  | 5aa42e5e66 | ||
|  | 708672ec7e | ||
|  | d2cefbf224 | ||
|  | adb7aa6950 | ||
|  | cd35ead890 | ||
|  | 9dc804ee27 | ||
|  | a8ceeaa7b0 | ||
|  | 7092f7663e | ||
|  | d9d2edeb08 | ||
|  | dda1ddcb26 | ||
|  | f0c890f160 | ||
|  | 4f52d43347 | ||
|  | 0ed7db979b | ||
|  | 9c78049359 | ||
|  | 7882105661 | ||
|  | c000e1d6dd | ||
|  | c5069edc78 | ||
|  | 282d9e138c | ||
|  | 72fcf2cbe1 | ||
|  | 6f49f5465b | ||
|  | 17b8bd8316 | ||
|  | 9b6b9c1fa2 | ||
|  | 609a2ca592 | ||
|  | 6dabf24bf3 | ||
|  | 7e88938932 | ||
|  | c707e64685 | ||
|  | a639690716 | ||
|  | 01222dbab7 | ||
|  | 93e2506279 | ||
|  | f62d5d3b9d | ||
|  | 0665acd190 | ||
|  | fea05e9d33 | ||
|  | 7a03c7d56f | ||
|  | 2dc2aec954 | ||
|  | 39c6c2417a | ||
|  | ff72d6a146 | ||
|  | 603d0d0c7c | ||
|  | 28883f711b | ||
|  | e914828add | ||
|  | c1480029fb | ||
|  | 40f622949e | ||
|  | 63096ac2bc | ||
|  | 03d5a0ec1d | ||
|  | 1c873e0034 | ||
|  | bcb47c306c | ||
|  | 01c4d3c225 | ||
|  | c2aaae4818 | ||
|  | 3f678e218d | ||
|  | c2a59cb476 | ||
|  | f8a1bd4e79 | ||
|  | d6e039a1d1 | ||
|  | 0f1a7c2b69 | ||
|  | 40ad9f4911 | ||
|  | 4116caff6a | ||
|  | 0b69f72315 | ||
|  | c569f5ddcf | ||
|  | 62f9e181e0 | ||
|  | 235a97ea10 | ||
|  | e541ae400c | ||
|  | 4822abde86 | ||
|  | b7e52812f8 | ||
|  | 69118120d9 | ||
|  | 7cba0c6fb0 | ||
|  | 5fac67ce15 | ||
|  | 98c733108e | ||
|  | 782186e13d | ||
|  | 4e1f6518e8 | ||
|  | 53e0fe8e51 | ||
|  | 0e547390da | ||
|  | 86b52df839 | ||
|  | d685fdf54a | ||
|  | d9caab4108 | ||
|  | 44b68f140e | ||
|  | 3a3d97dfa7 | ||
|  | 47898b527c | ||
|  | a35f36ad39 | ||
|  | d13a397f8e | ||
|  | df999723f8 | ||
|  | 8236e840a7 | ||
|  | e5b3625f73 | ||
|  | 2e4645310b | ||
|  | 50a32b387e | ||
|  | 2059283707 | ||
|  | 8e3af515c9 | ||
|  | 6f88f0ea3f | ||
|  | d2f37cf3f9 | ||
|  | 7c30d6254e | ||
|  | 64fb39a653 | ||
|  | 91895aa70c | ||
|  | 68dfaf238b | ||
|  | ebf13a0ba0 | ||
|  | 2bff9937b7 | ||
|  | 256395c28d | ||
|  | 3346bc8bba | ||
|  | 6fe22a7e62 | ||
|  | 757b98748b | ||
|  | 7a778f3f33 | ||
|  | 41d9059a2f | ||
|  | e26e0d7c01 | ||
|  | ad41c07a1f | ||
|  | 9576d246ee | ||
|  | 988d3ea8ba | ||
|  | 0767b92b62 | ||
|  | 5732f3b044 | ||
|  | 712115b6ce | ||
|  | 9283559c6b | ||
|  | 6b393438e9 | ||
|  | 2064abe16d | ||
|  | b605982f94 | ||
|  | 343b9ab455 | ||
|  | dcb226b202 | ||
|  | 2243021b58 | ||
|  | d5134e88b1 | ||
|  | c59adf612f | ||
|  | 93b628d9a8 | ||
|  | 6bac551d9f | ||
|  | 70a35656e4 | ||
|  | 047c18eac0 | ||
|  | b4a86ce6cf | ||
|  | a82d8ea0c3 | ||
|  | b778eed419 | ||
|  | ad57faa9a9 | ||
|  | a9b5e8d036 | ||
|  | 8be704e591 | ||
|  | b622a8fa58 | ||
|  | a519e5c475 | ||
|  | d620b6dd5e | ||
|  | 99335d986e | ||
|  | 7895cd92cd | ||
|  | 8b2c032da6 | ||
|  | da336247eb | ||
|  | dabd27d4be | ||
|  | fdda47db6e | ||
|  | efa6fd03e5 | ||
|  | 9e3e34acf5 | ||
|  | a2d0c1bf18 | ||
|  | 7663716ae8 | ||
|  | c2cacb3478 | ||
|  | 84666b54b9 | ||
|  | 2b91c23bf3 | ||
|  | 3297267a16 | ||
|  | a9e653724c | ||
|  | 5e79a1f500 | ||
|  | d4ff98680a | ||
|  | ba8d255cb4 | ||
|  | 06f4ad922c | ||
|  | bff06e448b | ||
|  | d48ffa2913 | ||
|  | d97c3a7e01 | ||
|  | 0b1161f7ef | ||
|  | 061e1a471d | ||
|  | a39d874600 | ||
|  | de96376565 | ||
|  | c54c20ab3c | ||
|  | 70fafa473b | ||
|  | 2e436eae6b | ||
|  | fd7e861ff5 | ||
|  | 792108686c | ||
|  | fa1b5117fd | ||
|  | b0bd9e0a34 | ||
|  | 05dc97099a | ||
|  | 9de61fcf58 | ||
|  | 7f7175b184 | ||
|  | cf5c640ae4 | ||
|  | 6b9371d105 | ||
|  | 9a82057303 | ||
|  | 48584e94c4 | ||
|  | d8024a5928 | ||
|  | 2034ab4f6c | ||
|  | 58b70b42dd | ||
|  | 1496bc1b07 | ||
|  | bfbf88b2ea | ||
|  | e621b938e3 | ||
|  | 59e6e798dd | ||
|  | e5c2dbc7ec | ||
|  | 756f71c382 | ||
|  | b7535693fa | ||
|  | 06a3505698 | ||
|  | 0372d17a11 | ||
|  | 4525588116 | ||
|  | 68e957c147 | ||
|  | 99f5ed1461 | ||
|  | 59f67796dc | ||
|  | aafdfa933e | ||
|  | 3208c8ed1e | ||
|  | 6bf733e24e | ||
|  | 65d3e8fbfc | ||
|  | a29d65d47c | ||
|  | efa8f0730d | ||
|  | 0af1edefff | ||
|  | 023d26f521 | ||
|  | 5068619f1b | ||
|  | 5b2457af0b | ||
|  | 900b4f1af9 | ||
|  | 4c22a98b0b | ||
|  | 3b8ca80900 | ||
|  | dc6eff83ea | ||
|  | 38ff66debd | ||
|  | 1d2e0f74ea | ||
|  | bf60e40d0b | ||
|  | c9094ca537 | ||
|  | 68b3fd6b8f | ||
|  | 9323b3a248 | ||
|  | b55e9329d9 | ||
|  | a5b4105971 | ||
|  | d1feaa935d | ||
|  | 6919930aaa | ||
|  | 69633826bb | ||
|  | 771162bfb1 | ||
|  | ba785e29e9 | ||
|  | 2c7b104f4a | ||
|  | 78951c197a | ||
|  | 07c1cf7137 | ||
|  | d26141151a | ||
|  | f59dbe4a88 | ||
|  | 8dae7f8225 | ||
|  | 5811389891 | ||
|  | debcaf6fb7 | ||
|  | b8d10a62c2 | ||
|  | d2b209234f | ||
|  | 34c9d8be50 | ||
|  | ae57ad0c81 | ||
|  | 0c1520dd9c | ||
|  | d594f43ebd | ||
|  | 125c693e3f | ||
|  | ad2f857e15 | ||
|  | e445d6aada | ||
|  | 88fbb0ffbb | ||
|  | 231908fe9f | ||
|  | f137cc10f4 | ||
|  | 1a8f8adc2a | ||
|  | 7a242bb4ed | ||
|  | 3b8bb09ae3 | ||
|  | 140db85d21 | ||
|  | ccce4b19e8 | ||
|  | 8cb9be7560 | ||
|  | 953f0569fb | ||
|  | 34c229fd33 | ||
|  | 958ad0d750 | ||
|  | 36ddd9dd69 | ||
|  | 38259c96c9 | ||
|  | c054fb8a2c | ||
|  | 5a0b8328d8 | ||
|  | ffa19426d7 | ||
|  | c123804294 | ||
|  | 4e24551b90 | ||
|  | 51cb5da7f0 | ||
|  | b528f48417 | ||
|  | ec7a79049a | ||
|  | 6ddad6b299 | ||
|  | 16dc7762f9 | ||
|  | 41f84447cc | ||
|  | ce073a704b | ||
|  | 113232ebb6 | ||
|  | dc0ed8857f | ||
|  | bb6b77bd98 | ||
|  | dcc80f9032 | ||
|  | dd554bcdf4 | ||
|  | f376a39e55 | ||
|  | 8dcc9d6b66 | ||
|  | a13a1225b7 | ||
|  | 0ec84be5da | ||
|  | b1cefb7e3e | ||
|  | cc0c1c08b9 | ||
|  | 3a67884451 | ||
|  | 72e716cdf1 | ||
|  | 40e06c9819 | ||
|  | ad6c5ff11d | ||
|  | 335512e232 | ||
|  | 2622e59b0b | ||
|  | 35e6a13cd1 | ||
|  | a576c9f21f | ||
|  | b48490badc | ||
|  | 71a438e2cb | ||
|  | 272d6f2a8b | ||
|  | 5c22065135 | ||
|  | e7dd6c52ac | ||
|  | 3bf042dce9 | ||
|  | 64f798d4b2 | ||
|  | f43e04e15a | ||
|  | 88d72f8c9a | ||
|  | 9826726a72 | ||
|  | c66d0550e8 | ||
|  | 4aeacfd16e | ||
|  | 58fa63ad88 | ||
|  | 94f944dc9c | ||
|  | 116ddbdd01 | ||
|  | 1c0697b5d4 | ||
|  | 434ca47ea0 | ||
|  | 397ef72b16 | ||
|  | 7ca9245735 | ||
|  | 69856286e8 | ||
|  | ad43d6a5bc | ||
|  | 1e5004f495 | ||
|  | 253161d3d0 | ||
|  | ab47e201c7 | ||
|  | 42984fa72a | ||
|  | e7864a28a1 | ||
|  | 21803607e7 | ||
|  | 62b366a5ec | ||
|  | 2b39988707 | ||
|  | f9e7291050 | ||
|  | 4de642ff28 | ||
|  | 0384efcfc2 | ||
|  | bf91443f38 | ||
|  | 4a5970b4af | ||
|  | e3fd68c849 | ||
|  | df0de2fc2d | ||
|  | 0c3568fad5 | ||
|  | 976f5d91ed | ||
|  | 0f3d4d9a47 | ||
|  | ad1f4429c9 | ||
|  | 7590d5eacb | ||
|  | c5974b8833 | ||
|  | 511c8de6f3 | ||
|  | a718ac7ee0 | ||
|  | ef832becf1 | ||
|  | 3a62455948 | ||
|  | 297824e2d7 | ||
|  | d92f297bc0 | ||
|  | 7a0827e3d0 | ||
|  | ef256a64b8 | ||
|  | 1de941e837 | ||
|  | 28b65cb810 | ||
|  | 6ff3942e8b | ||
|  | ef5d959788 | ||
|  | 6a2c58fcc0 | ||
|  | 4e6bdb31ac | ||
|  | 80d03a631e | ||
|  | 6b27f2d2cf | ||
|  | 7cb6729fa7 | ||
|  | 2f46267994 | ||
|  | cdda648360 | ||
|  | f2d677d51a | ||
|  | c2ee0f0864 | ||
|  | 2a84db7f85 | ||
|  | 8187a4bce9 | ||
|  | 97681d142e | ||
|  | b2430097f2 | ||
|  | 7da12a878f | ||
|  | a31700e16f | ||
|  | 7854522792 | ||
|  | a6a9ebfde2 | ||
|  | c6cbe2748e | ||
|  | f9a7f00843 | ||
|  | f0b183a552 | ||
|  | 338ada5c9f | ||
|  | ef88f9923f | ||
|  | 6f8c7d9ec4 | ||
|  | ec769ccf72 | ||
|  | 045952939e | ||
|  | 1c51cac5ba | ||
|  | ea11462e1e | ||
|  | 62f9736b1d | ||
|  | 1f8a1f0046 | ||
|  | 172507acb5 | ||
|  | 434ab65c16 | ||
|  | cb5f793ede | ||
|  | 5dc776e55f | ||
|  | 72d60f30f7 | ||
|  | 869743a742 | ||
|  | 7b03e07908 | ||
|  | 348f880e15 | ||
|  | 737188ae50 | ||
|  | db21731b14 | ||
|  | cdb4fa2487 | ||
|  | 514204f0d4 | ||
|  | ead597d0fb | ||
|  | afbf989715 | ||
|  | 01b62a16c3 | ||
|  | c5eba04517 | ||
|  | 282313ab52 | ||
|  | d274545e77 | ||
|  | 45ac577c4d | ||
|  | 09402fdb22 | ||
|  | 89e7448007 | ||
|  | 1ea6f957bc | ||
|  | f44fca0a4b | ||
|  | 52d2f62a57 | ||
|  | d3fda37615 | ||
|  | cbe3092404 | ||
|  | 6dfe3039d0 | ||
|  | d6009453df | ||
|  | 2a8668ea60 | ||
|  | cc0d433621 | ||
|  | c81323ef91 | ||
|  | 1fe89fb364 | ||
|  | 961c27f1c2 | ||
|  | fe4a14e6cc | ||
|  | ee58ad1ac0 | ||
|  | c0ff899812 | ||
|  | d9c938de33 | ||
|  | 56547b3d50 | ||
|  | 5026bc7a78 | ||
|  | 27364ee72c | ||
|  | ece71a0228 | ||
|  | 073828235f | ||
|  | 41bcc8c0f4 | ||
|  | a0ea2aae6e | ||
|  | f34b46a621 | ||
|  | 7217a4f7a4 | ||
|  | 6383eca54a | ||
|  | e55bd1e559 | ||
|  | 9e8b701dea | ||
|  | a4431abea8 | ||
|  | 5844c1767b | ||
|  | 9a70bfa471 | ||
|  | b406c6403c | ||
|  | 499625f266 | ||
|  | 6b773553fc | ||
|  | 15fe049a99 | ||
|  | e4555f6997 | ||
|  | 470071e0b0 | ||
|  | ea1be8e7bf | ||
|  | 84a830195f | ||
|  | e62c3e00c1 | ||
|  | 07e790f900 | ||
|  | 640142fc0c | ||
|  | 5c339d4597 | ||
|  | a4931f5d78 | ||
|  | 5e1e543b06 | ||
|  | df929f9445 | ||
|  | d8e719d1c4 | ||
|  | 3067e482fc | ||
|  | ed5930e934 | ||
|  | ffea3597f4 | ||
|  | 193d3e0206 | ||
|  | c8f4fbb7dd | ||
|  | c855bc31b4 | ||
|  | b924b179ab | ||
|  | 3df0fee3de | ||
|  | b601560e81 | ||
|  | e5775cf812 | ||
|  | 26dd1f8532 | ||
|  | 5143a5b5c5 | ||
|  | 15ce27992e | ||
|  | dbc2812022 | ||
|  | dce3713f12 | ||
|  | f849d45bb6 | ||
|  | 8ad06fb9ea | ||
|  | 9124d9d6e6 | ||
|  | 45ebe51e4f | ||
|  | 407661d56b | ||
|  | 998d4229af | ||
|  | a02d2e2e11 | ||
|  | 72fa68849f | ||
|  | 33f17f75a0 | ||
|  | 23edb18d7e | ||
|  | 07ff3a853f | ||
|  | 2cf36bdb46 | ||
|  | 50848c2f4d | ||
|  | d32633b3c7 | ||
|  | b37739eec2 | ||
|  | 28f87dc804 | ||
|  | 41879e41e6 | ||
|  | fc0a6546a2 | ||
|  | ffd4280d6c | ||
|  | f859b346a6 | ||
|  | cb0677cafe | ||
|  | c6956527d1 | ||
|  | 72c6bfaa50 | ||
|  | 7927b5f624 | ||
|  | b7aad39daf | ||
|  | f48de6dd43 | ||
|  | 79d73d8f8b | ||
|  | cc5947467f | ||
|  | e152f128c8 | ||
|  | 99bd808ebe | ||
|  | beb5f3dc9d | ||
|  | f5c3b3446f | ||
|  | db3b955b0f | ||
|  | f431c7402f | ||
|  | 5516f65971 | ||
|  | 9471df0a1b | ||
|  | 6d39f64be7 | ||
|  | 4907e6f6d7 | ||
|  | 1ccee86705 | ||
|  | 542fb2175b | ||
|  | 6ec9cfb044 | ||
|  | 66e0ff8392 | ||
|  | 1fb0a7109d | ||
|  | b89d0a9a73 | ||
|  | 4bb779d9a5 | ||
|  | 386a5b6362 | ||
|  | e32a999cd0 | ||
|  | 192eb49589 | ||
|  | 5d70ff702b | ||
|  | a7b05db2a1 | ||
|  | 45e346cf1b | ||
|  | 80e2bfada3 | ||
|  | 16e7bd0388 | ||
|  | b3fb35783e | ||
|  | a79c6aa9e0 | ||
|  | 4bb58b2de9 | ||
|  | 4e10881331 | ||
|  | cec4a81e14 | ||
|  | da45923d05 | ||
|  | 31a61b598b | ||
|  | 9c0506592b | ||
|  | beeb0c7c5a | ||
|  | b2f05faee0 | ||
|  | bfbc6a4bad | ||
|  | 8c9e0e552d | ||
|  | 8375e1d64d | ||
|  | cf5193d3e5 | ||
|  | c490388e80 | ||
|  | 24ec5a6e9d | ||
|  | 6df1d5222d | ||
|  | 58fb7a02f6 | ||
|  | 3d51ac8df0 | ||
|  | 6fe4ff7f85 | ||
|  | 2253d4bc16 | ||
|  | e5cc19de43 | ||
|  | 5404617d43 | ||
|  | 12467a18e6 | ||
|  | 1db7043a4d | ||
|  | 49932747b3 | ||
|  | 55db190875 | ||
|  | 71fe2f7ed3 | ||
|  | ffc112c9d0 | ||
|  | d3e48e296f | ||
|  | 14f6ae75ea | ||
|  | c84efe64d3 | ||
|  | 10e89a7dbb | ||
|  | ef44acbf10 | ||
|  | 06da540ab0 | ||
|  | 40c017fd54 | ||
|  | f0bcf81a98 | ||
|  | 6a0b343289 | 
							
								
								
									
										10
									
								
								.clang-tidy
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.clang-tidy
									
									
									
									
									
								
							| @@ -68,8 +68,6 @@ Checks: >- | ||||
|   -modernize-use-nodiscard, | ||||
|   -mpi-*, | ||||
|   -objc-*, | ||||
|   -readability-braces-around-statements, | ||||
|   -readability-const-return-type, | ||||
|   -readability-convert-member-functions-to-static, | ||||
|   -readability-else-after-return, | ||||
|   -readability-function-cognitive-complexity, | ||||
| @@ -77,10 +75,6 @@ Checks: >- | ||||
|   -readability-isolate-declaration, | ||||
|   -readability-magic-numbers, | ||||
|   -readability-make-member-function-const, | ||||
|   -readability-named-parameter, | ||||
|   -readability-qualified-auto, | ||||
|   -readability-redundant-access-specifiers, | ||||
|   -readability-redundant-member-init, | ||||
|   -readability-redundant-string-init, | ||||
|   -readability-uppercase-literal-suffix, | ||||
|   -readability-use-anyofallof, | ||||
| @@ -114,6 +108,8 @@ CheckOptions: | ||||
|     value:           'make_unique' | ||||
|   - key:             modernize-make-unique.MakeSmartPtrFunctionHeader | ||||
|     value:           'esphome/core/helpers.h' | ||||
|   - key:             readability-braces-around-statements.ShortStatementLines | ||||
|     value:           2 | ||||
|   - key:             readability-identifier-naming.LocalVariableCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.ClassCase | ||||
| @@ -160,3 +156,5 @@ CheckOptions: | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.VirtualMethodSuffix | ||||
|     value:           '' | ||||
|   - key:             readability-qualified-auto.AddConstToQualified | ||||
|     value:           0 | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,4 +9,3 @@ contact_links: | ||||
|   - name: Frequently Asked Question | ||||
|     url: https://esphome.io/guides/faq.html | ||||
|     about: Please view the FAQ for common questions and what to include in a bug report. | ||||
|      | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| # What does this implement/fix?  | ||||
| # What does this implement/fix? | ||||
|  | ||||
| Quick description and explanation of changes | ||||
|  | ||||
| @@ -35,6 +35,6 @@ Quick description and explanation of changes | ||||
| ## Checklist: | ||||
|   - [ ] The code change is tested and works locally. | ||||
|   - [ ] Tests have been added to verify that the new code works (under `tests/` folder). | ||||
|    | ||||
|  | ||||
| If user exposed functionality or configuration variables are added/changed: | ||||
|   - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). | ||||
|   | ||||
							
								
								
									
										22
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -51,26 +51,26 @@ jobs: | ||||
|             name: Run script/clang-format | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP8266 | ||||
|             options: --environment esp8266-tidy --grep USE_ESP8266 | ||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||
|             pio_cache_key: tidyesp8266 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 1/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 1 | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 1/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 1 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 2/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 2 | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 2/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 2 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 3/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 3 | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 3/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 3 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 4/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 4 | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 4/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 esp-idf | ||||
|             name: Run script/clang-tidy for ESP32 IDF | ||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||
|             pio_cache_key: tidyesp32-idf | ||||
|  | ||||
| @@ -80,7 +80,7 @@ jobs: | ||||
|         uses: actions/setup-python@v2 | ||||
|         id: python | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|           python-version: '3.8' | ||||
|  | ||||
|       - name: Cache virtualenv | ||||
|         uses: actions/cache@v2 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -137,18 +137,18 @@ jobs: | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           manifest | ||||
|  | ||||
|   deploy-hassio-repo: | ||||
|   deploy-ha-addon-repo: | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [deploy-docker] | ||||
|     steps: | ||||
|       - env: | ||||
|           TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} | ||||
|           TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||
|         run: | | ||||
|           TAG="${GITHUB_REF#refs/tags/}" | ||||
|           curl \ | ||||
|             -u ":$TOKEN" \ | ||||
|             -X POST \ | ||||
|             -H "Accept: application/vnd.github.v3+json" \ | ||||
|             https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ | ||||
|             https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \ | ||||
|             -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -77,6 +77,7 @@ venv/ | ||||
| ENV/ | ||||
| env.bak/ | ||||
| venv.bak/ | ||||
| venv-*/ | ||||
|  | ||||
| # mypy | ||||
| .mypy_cache/ | ||||
|   | ||||
| @@ -3,4 +3,4 @@ ports: | ||||
|   onOpen: open-preview | ||||
| tasks: | ||||
| - before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup | ||||
|   command: python -m esphome config dashboard | ||||
|   command: python -m esphome dashboard config | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/ambv/black | ||||
|     rev: 20.8b1 | ||||
|     rev: 22.3.0 | ||||
|     hooks: | ||||
|     - id: black | ||||
|       args: | ||||
| @@ -10,7 +10,7 @@ repos: | ||||
|         - --quiet | ||||
|       files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|   - repo: https://gitlab.com/pycqa/flake8 | ||||
|     rev: 3.8.4 | ||||
|     rev: 4.0.1 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
|         additional_dependencies: | ||||
| @@ -25,3 +25,8 @@ repos: | ||||
|           - --branch=dev | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v2.31.1 | ||||
|     hooks: | ||||
|       - id: pyupgrade | ||||
|         args: [--py38-plus] | ||||
|   | ||||
							
								
								
									
										59
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -19,6 +19,7 @@ esphome/components/airthings_wave_mini/* @ncareau | ||||
| esphome/components/airthings_wave_plus/* @jeromelaban | ||||
| esphome/components/am43/* @buxtronix | ||||
| esphome/components/am43/cover/* @buxtronix | ||||
| esphome/components/analog_threshold/* @ianchi | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| esphome/components/api/* @OttoWinter | ||||
| @@ -27,18 +28,25 @@ esphome/components/atc_mithermometer/* @ahpohl | ||||
| esphome/components/b_parasite/* @rbaron | ||||
| esphome/components/ballu/* @bazuchan | ||||
| esphome/components/bang_bang/* @OttoWinter | ||||
| esphome/components/bedjet/* @jhansche | ||||
| esphome/components/bh1750/* @OttoWinter | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/ble_client/* @buxtronix | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/bmp3xx/* @martgras | ||||
| esphome/components/button/* @esphome/core | ||||
| esphome/components/canbus/* @danielschramm @mvturnho | ||||
| esphome/components/cap1188/* @MrEditor97 | ||||
| esphome/components/captive_portal/* @OttoWinter | ||||
| esphome/components/ccs811/* @habbie | ||||
| esphome/components/cd74hc4067/* @asoehlke | ||||
| esphome/components/climate/* @esphome/core | ||||
| esphome/components/climate_ir/* @glmnet | ||||
| esphome/components/color_temperature/* @jesserockz | ||||
| esphome/components/coolix/* @glmnet | ||||
| esphome/components/copy/* @OttoWinter | ||||
| esphome/components/cover/* @esphome/core | ||||
| esphome/components/cs5460a/* @balrog-kun | ||||
| esphome/components/cse7761/* @berfenger | ||||
| @@ -47,14 +55,18 @@ esphome/components/current_based/* @djwmarcx | ||||
| esphome/components/daly_bms/* @s1lvi0 | ||||
| esphome/components/dashboard_import/* @esphome/core | ||||
| esphome/components/debug/* @OttoWinter | ||||
| esphome/components/delonghi/* @grob6000 | ||||
| esphome/components/dfplayer/* @glmnet | ||||
| esphome/components/dht/* @OttoWinter | ||||
| esphome/components/ds1307/* @badbadc0ffee | ||||
| esphome/components/dsmr/* @glmnet @zuidwijk | ||||
| esphome/components/ektf2232/* @jesserockz | ||||
| esphome/components/ens210/* @itn3rd77 | ||||
| esphome/components/esp32/* @esphome/core | ||||
| esphome/components/esp32_ble/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| @@ -65,25 +77,36 @@ esphome/components/globals/* @esphome/core | ||||
| esphome/components/gpio/* @esphome/core | ||||
| esphome/components/gps/* @coogle | ||||
| esphome/components/graph/* @synco | ||||
| esphome/components/growatt_solar/* @leeuwte | ||||
| esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/honeywellabp/* @RubyBailey | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/i2s_audio/* @jesserockz | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| esphome/components/ina260/* @MrEditor97 | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| esphome/components/inkplate6/* @jesserockz | ||||
| esphome/components/integration/* @OttoWinter | ||||
| esphome/components/interval/* @esphome/core | ||||
| esphome/components/json/* @OttoWinter | ||||
| esphome/components/kalman_combinator/* @Cat-Ion | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/light/* @esphome/core | ||||
| esphome/components/lilygo_t5_47/touchscreen/* @jesserockz | ||||
| esphome/components/lock/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
| esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/max9611/* @mckaymatthew | ||||
| esphome/components/mcp23008/* @jesserockz | ||||
| esphome/components/mcp23017/* @jesserockz | ||||
| esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | ||||
| @@ -92,18 +115,28 @@ esphome/components/mcp23x08_base/* @jesserockz | ||||
| esphome/components/mcp23x17_base/* @jesserockz | ||||
| esphome/components/mcp23xxx_base/* @jesserockz | ||||
| esphome/components/mcp2515/* @danielschramm @mvturnho | ||||
| esphome/components/mcp3204/* @rsumner | ||||
| esphome/components/mcp4728/* @berfenger | ||||
| esphome/components/mcp47a1/* @jesserockz | ||||
| esphome/components/mcp9808/* @k7hpn | ||||
| esphome/components/md5/* @esphome/core | ||||
| esphome/components/mdns/* @esphome/core | ||||
| esphome/components/media_player/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mlx90393/* @functionpointer | ||||
| esphome/components/modbus_controller/* @martgras | ||||
| esphome/components/modbus_controller/binary_sensor/* @martgras | ||||
| esphome/components/modbus_controller/number/* @martgras | ||||
| esphome/components/modbus_controller/output/* @martgras | ||||
| esphome/components/modbus_controller/select/* @martgras @stegm | ||||
| esphome/components/modbus_controller/sensor/* @martgras | ||||
| esphome/components/modbus_controller/switch/* @martgras | ||||
| esphome/components/modbus_controller/text_sensor/* @martgras | ||||
| esphome/components/mopeka_ble/* @spbrogan | ||||
| esphome/components/mopeka_pro_check/* @spbrogan | ||||
| esphome/components/mpu6886/* @fabaff | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @senexcrenshaw | ||||
| esphome/components/nextion/binary_sensor/* @senexcrenshaw | ||||
| @@ -123,8 +156,13 @@ esphome/components/pn532_i2c/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532_spi/* @OttoWinter @jesserockz | ||||
| esphome/components/power_supply/* @esphome/core | ||||
| esphome/components/preferences/* @esphome/core | ||||
| esphome/components/pulse_meter/* @stevebaxter | ||||
| esphome/components/psram/* @esphome/core | ||||
| esphome/components/pulse_meter/* @cstaahl @stevebaxter | ||||
| esphome/components/pvvx_mithermometer/* @pasiz | ||||
| esphome/components/qmp6988/* @andrewpc | ||||
| esphome/components/qr_code/* @wjtje | ||||
| 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 | ||||
| @@ -132,21 +170,28 @@ esphome/components/restart/* @esphome/core | ||||
| esphome/components/rf_bridge/* @jesserockz | ||||
| esphome/components/rgbct/* @jesserockz | ||||
| esphome/components/rtttl/* @glmnet | ||||
| esphome/components/safe_mode/* @paulmonigatti | ||||
| esphome/components/scd4x/* @sjtrny | ||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | ||||
| esphome/components/scd4x/* @martgras @sjtrny | ||||
| esphome/components/script/* @esphome/core | ||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||
| esphome/components/sdp3x/* @Azimath | ||||
| esphome/components/selec_meter/* @sourabhjaiswal | ||||
| esphome/components/select/* @esphome/core | ||||
| esphome/components/sen5x/* @martgras | ||||
| esphome/components/sensirion_common/* @martgras | ||||
| esphome/components/sensor/* @esphome/core | ||||
| esphome/components/sgp40/* @SenexCrenshaw | ||||
| esphome/components/sgp4x/* @SenexCrenshaw @martgras | ||||
| esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||
| esphome/components/sht4x/* @sjtrny | ||||
| esphome/components/shutdown/* @esphome/core | ||||
| esphome/components/shutdown/* @esphome/core @jsuanet | ||||
| esphome/components/sim800l/* @glmnet | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 | ||||
| esphome/components/sml/* @alengwenus | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||
| esphome/components/spi/* @esphome/core | ||||
| esphome/components/sps30/* @martgras | ||||
| esphome/components/ssd1322_base/* @kbx81 | ||||
| esphome/components/ssd1322_spi/* @kbx81 | ||||
| esphome/components/ssd1325_base/* @kbx81 | ||||
| @@ -176,17 +221,23 @@ esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| esphome/components/toshiba/* @kbx81 | ||||
| esphome/components/touchscreen/* @jesserockz | ||||
| esphome/components/tsl2591/* @wjcarpenter | ||||
| esphome/components/tuya/binary_sensor/* @jesserockz | ||||
| esphome/components/tuya/climate/* @jesserockz | ||||
| esphome/components/tuya/number/* @frankiboy1 | ||||
| esphome/components/tuya/select/* @bearpawmaxim | ||||
| esphome/components/tuya/sensor/* @jesserockz | ||||
| esphome/components/tuya/switch/* @jesserockz | ||||
| esphome/components/tuya/text_sensor/* @dentra | ||||
| esphome/components/uart/* @esphome/core | ||||
| esphome/components/ultrasonic/* @OttoWinter | ||||
| esphome/components/version/* @esphome/core | ||||
| esphome/components/wake_on_lan/* @willwill2will54 | ||||
| esphome/components/web_server_base/* @OttoWinter | ||||
| esphome/components/whirlpool/* @glmnet | ||||
| esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||
| esphome/components/xpt2046/* @numo68 | ||||
|   | ||||
| @@ -5,7 +5,7 @@ For a detailed guide, please see https://esphome.io/guides/contributing.html#con | ||||
| Things to note when contributing: | ||||
|  | ||||
|  - Please test your changes :) | ||||
|  - If a new feature is added or an existing user-facing feature is changed, you should also  | ||||
|  - 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 | ||||
|   | ||||
| @@ -4,4 +4,5 @@ include requirements.txt | ||||
| include esphome/dashboard/templates/*.html | ||||
| recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE | ||||
| recursive-include esphome *.cpp *.h *.tcc | ||||
| recursive-include esphome *.py.script | ||||
| recursive-include esphome LICENSE.txt | ||||
|   | ||||
| @@ -5,12 +5,14 @@ | ||||
| # One of "docker", "hassio" | ||||
| ARG BASEIMGTYPE=docker | ||||
|  | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.1 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.1 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.1 AS base-hassio-armv7 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-armv7 | ||||
| # https://github.com/hassio-addons/addon-debian-base/releases | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7 | ||||
| # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye | ||||
| FROM debian:bullseye-20220328-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20220328-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20220328-slim AS base-docker-armv7 | ||||
|  | ||||
| # Use TARGETARCH/TARGETVARIANT defined by docker | ||||
| # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope | ||||
| @@ -21,13 +23,14 @@ RUN \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         python3=3.9.2-3 \ | ||||
|         python3-pip=20.3.4-4 \ | ||||
|         python3-pip=20.3.4-4+deb11u1 \ | ||||
|         python3-setuptools=52.0.0-4 \ | ||||
|         python3-pil=8.1.2+dfsg-0.3 \ | ||||
|         python3-pil=8.1.2+dfsg-0.3+deb11u1 \ | ||||
|         python3-cryptography=3.3.2-1 \ | ||||
|         iputils-ping=3:20210202-1 \ | ||||
|         git=1:2.30.2-1 \ | ||||
|         curl=7.74.0-1.3+b1 \ | ||||
|         curl=7.74.0-1.3+deb11u1 \ | ||||
|         openssh-client=1:8.4p1-5 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -42,8 +45,8 @@ ENV \ | ||||
| RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     pip3 install --no-cache-dir \ | ||||
|         wheel==0.36.2 \ | ||||
|         platformio==5.2.2 \ | ||||
|         wheel==0.37.1 \ | ||||
|         platformio==5.2.5 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_libraries_interval 1000000 \ | ||||
| @@ -52,19 +55,19 @@ RUN \ | ||||
|     && mkdir -p /piolibs | ||||
|  | ||||
|  | ||||
|  | ||||
| # ======================= docker-type image ======================= | ||||
| FROM base AS docker | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|  | ||||
|  | ||||
| # ======================= docker-type image ======================= | ||||
| FROM base AS docker | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN pip3 install --no-cache-dir -e /esphome | ||||
| RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome | ||||
|  | ||||
| # Settings for dashboard | ||||
| ENV USERNAME="" PASSWORD="" | ||||
| @@ -93,7 +96,7 @@ RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         nginx=1.18.0-6.1 \ | ||||
|         nginx-light=1.18.0-6.1 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -102,17 +105,11 @@ RUN \ | ||||
| ARG BUILD_VERSION=dev | ||||
|  | ||||
| # Copy root filesystem | ||||
| COPY docker/hassio-rootfs/ / | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
| COPY docker/ha-addon-rootfs/ / | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN pip3 install --no-cache-dir -e /esphome | ||||
| RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome | ||||
|  | ||||
| # Labels | ||||
| LABEL \ | ||||
| @@ -147,10 +144,8 @@ RUN \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
| COPY requirements_test.txt / | ||||
| RUN pip3 install --no-cache-dir -r /requirements_test.txt | ||||
|  | ||||
| VOLUME ["/esphome"] | ||||
| WORKDIR /esphome | ||||
|   | ||||
| @@ -32,6 +32,7 @@ parser.add_argument("--dry-run", action="store_true", help="Don't run any comman | ||||
| subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) | ||||
| build_parser = subparsers.add_parser("build", help="Build the image") | ||||
| build_parser.add_argument("--push", help="Also push the images", action="store_true") | ||||
| build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") | ||||
| manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") | ||||
|  | ||||
|  | ||||
| @@ -132,6 +133,8 @@ def main(): | ||||
|             cmd += ["--tag", img] | ||||
|         if args.push: | ||||
|             cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"] | ||||
|         if args.load: | ||||
|             cmd += ["--load"] | ||||
|  | ||||
|         run_command(*cmd, ".") | ||||
|     elif args.command == "manifest": | ||||
|   | ||||
| @@ -7,12 +7,12 @@ | ||||
| # Check SSL requirements, if enabled | ||||
| if bashio::config.true 'ssl'; then | ||||
|     if ! bashio::config.has_value 'certfile'; then | ||||
|         bashio::fatal 'SSL is enabled, but no certfile was specified.' | ||||
|         bashio::log.fatal 'SSL is enabled, but no certfile was specified.' | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
| 
 | ||||
|     if ! bashio::config.has_value 'keyfile'; then | ||||
|         bashio::fatal 'SSL is enabled, but no keyfile was specified' | ||||
|         bashio::log.fatal 'SSL is enabled, but no keyfile was specified' | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
| 
 | ||||
| @@ -10,7 +10,7 @@ server { | ||||
|     ssl_certificate_key /ssl/%%keyfile%%; | ||||
| 
 | ||||
|     # Clear Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress ""; | ||||
|     proxy_set_header X-HA-Ingress ""; | ||||
| 
 | ||||
|     # Redirect http requests to https on the same port. | ||||
|     # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ | ||||
| @@ -4,7 +4,7 @@ server { | ||||
|     include /etc/nginx/includes/server_params.conf; | ||||
|     include /etc/nginx/includes/proxy_params.conf; | ||||
|     # Clear Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress ""; | ||||
|     proxy_set_header X-HA-Ingress ""; | ||||
| 
 | ||||
|     location / { | ||||
|         proxy_pass http://esphome; | ||||
| @@ -3,8 +3,8 @@ server { | ||||
| 
 | ||||
|     include /etc/nginx/includes/server_params.conf; | ||||
|     include /etc/nginx/includes/proxy_params.conf; | ||||
|     # Set Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress "YES"; | ||||
|     # Set Home Assistant Ingress header | ||||
|     proxy_set_header X-HA-Ingress "YES"; | ||||
| 
 | ||||
|     location / { | ||||
|         # Only allow from Hass.io supervisor | ||||
| @@ -4,7 +4,7 @@ | ||||
| # Runs the ESPHome dashboard | ||||
| # ============================================================================== | ||||
| 
 | ||||
| export ESPHOME_IS_HASSIO=true | ||||
| export ESPHOME_IS_HA_ADDON=true | ||||
| 
 | ||||
| if bashio::config.true 'leave_front_door_open'; then | ||||
|     export DISABLE_HA_AUTHENTICATION=true | ||||
| @@ -32,4 +32,4 @@ export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" | ||||
| export PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
| 
 | ||||
| bashio::log.info "Starting ESPHome dashboard..." | ||||
| exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio | ||||
| exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon | ||||
| @@ -2,6 +2,7 @@ import argparse | ||||
| import functools | ||||
| import logging | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
| from datetime import datetime | ||||
|  | ||||
| @@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util | ||||
| import esphome.codegen as cg | ||||
| from esphome.config import iter_components, read_config, strip_default_ids | ||||
| from esphome.const import ( | ||||
|     ALLOWED_NAME_CHARS, | ||||
|     CONF_BAUD_RATE, | ||||
|     CONF_BROKER, | ||||
|     CONF_DEASSERT_RTS_DTR, | ||||
|     CONF_LOGGER, | ||||
|     CONF_NAME, | ||||
|     CONF_OTA, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PORT, | ||||
|     CONF_ESPHOME, | ||||
|     CONF_PLATFORMIO_OPTIONS, | ||||
|     CONF_SUBSTITUTIONS, | ||||
|     SECRETS_FILES, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, coroutine | ||||
| @@ -145,6 +149,8 @@ def wrap_to_code(name, comp): | ||||
|         if comp.config_schema is not None: | ||||
|             conf_str = yaml_util.dump(conf) | ||||
|             conf_str = conf_str.replace("//", "") | ||||
|             # remove tailing \ to avoid multi-line comment warning | ||||
|             conf_str = conf_str.replace("\\\n", "\n") | ||||
|             cg.add(cg.LineComment(indent(conf_str))) | ||||
|         await coro(conf) | ||||
|  | ||||
| @@ -479,6 +485,98 @@ def command_idedata(args, config): | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def command_rename(args, config): | ||||
|     for c in args.name: | ||||
|         if c not in ALLOWED_NAME_CHARS: | ||||
|             print( | ||||
|                 color( | ||||
|                     Fore.BOLD_RED, | ||||
|                     f"'{c}' is an invalid character for names. Valid characters are: " | ||||
|                     f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", | ||||
|                 ) | ||||
|             ) | ||||
|             return 1 | ||||
|     # Load existing yaml file | ||||
|     with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: | ||||
|         raw_contents = raw_file.read() | ||||
|  | ||||
|     yaml = yaml_util.load_yaml(CORE.config_path) | ||||
|     if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: | ||||
|         print( | ||||
|             color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") | ||||
|         ) | ||||
|         return 1 | ||||
|     old_name = yaml[CONF_ESPHOME][CONF_NAME] | ||||
|     match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) | ||||
|     if match is None: | ||||
|         new_raw = re.sub( | ||||
|             rf"name:\s+[\"']?{old_name}[\"']?", | ||||
|             f'name: "{args.name}"', | ||||
|             raw_contents, | ||||
|         ) | ||||
|     else: | ||||
|         old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] | ||||
|         if ( | ||||
|             len( | ||||
|                 re.findall( | ||||
|                     rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", | ||||
|                     raw_contents, | ||||
|                     flags=re.MULTILINE, | ||||
|                 ) | ||||
|             ) | ||||
|             > 1 | ||||
|         ): | ||||
|             print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) | ||||
|             return 1 | ||||
|  | ||||
|         new_raw = re.sub( | ||||
|             rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", | ||||
|             f'\\1: "{args.name}"', | ||||
|             raw_contents, | ||||
|             flags=re.MULTILINE, | ||||
|         ) | ||||
|  | ||||
|     new_path = os.path.join(CORE.config_dir, args.name + ".yaml") | ||||
|     print( | ||||
|         f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" | ||||
|     ) | ||||
|     print() | ||||
|  | ||||
|     with open(new_path, mode="w", encoding="utf-8") as new_file: | ||||
|         new_file.write(new_raw) | ||||
|  | ||||
|     rc = run_external_process("esphome", "config", new_path) | ||||
|     if rc != 0: | ||||
|         print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     cli_args = [ | ||||
|         "run", | ||||
|         new_path, | ||||
|         "--no-logs", | ||||
|         "--device", | ||||
|         CORE.address, | ||||
|     ] | ||||
|  | ||||
|     if args.dashboard: | ||||
|         cli_args.insert(0, "--dashboard") | ||||
|  | ||||
|     try: | ||||
|         rc = run_external_process("esphome", *cli_args) | ||||
|     except KeyboardInterrupt: | ||||
|         rc = 1 | ||||
|     if rc != 0: | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     os.remove(CORE.config_path) | ||||
|  | ||||
|     print(color(Fore.BOLD_GREEN, "SUCCESS")) | ||||
|     print() | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| PRE_CONFIG_ACTIONS = { | ||||
|     "wizard": command_wizard, | ||||
|     "version": command_version, | ||||
| @@ -497,6 +595,7 @@ POST_CONFIG_ACTIONS = { | ||||
|     "mqtt-fingerprint": command_mqtt_fingerprint, | ||||
|     "clean": command_clean, | ||||
|     "idedata": command_idedata, | ||||
|     "rename": command_rename, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -659,7 +758,7 @@ def parse_args(argv): | ||||
|         "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" | ||||
|     ) | ||||
|     parser_dashboard.add_argument( | ||||
|         "--hassio", help=argparse.SUPPRESS, action="store_true" | ||||
|         "--ha-addon", help=argparse.SUPPRESS, action="store_true" | ||||
|     ) | ||||
|     parser_dashboard.add_argument( | ||||
|         "--socket", help="Make the dashboard serve under a unix socket", type=str | ||||
| @@ -679,6 +778,15 @@ def parse_args(argv): | ||||
|         "configuration", help="Your YAML configuration file(s).", nargs=1 | ||||
|     ) | ||||
|  | ||||
|     parser_rename = subparsers.add_parser( | ||||
|         "rename", | ||||
|         help="Rename a device in YAML, compile the binary and upload it.", | ||||
|     ) | ||||
|     parser_rename.add_argument( | ||||
|         "configuration", help="Your YAML configuration file.", nargs=1 | ||||
|     ) | ||||
|     parser_rename.add_argument("name", help="The new name for the device.", type=str) | ||||
|  | ||||
|     # Keep backward compatibility with the old command line format of | ||||
|     # esphome <config> <command>. | ||||
|     # | ||||
| @@ -776,10 +884,10 @@ def run_esphome(argv): | ||||
|         _LOGGER.warning("Please instead use:") | ||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion)) | ||||
|  | ||||
|     if sys.version_info < (3, 7, 0): | ||||
|     if sys.version_info < (3, 8, 0): | ||||
|         _LOGGER.error( | ||||
|             "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " | ||||
|             "with this Python version. Please reinstall ESPHome with Python 3.7+" | ||||
|             "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 | ||||
|  | ||||
|   | ||||
| @@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| def validate_wait_until(value): | ||||
|     schema = cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|             cv.Optional(CONF_TIMEOUT): cv.templatable( | ||||
|                 cv.positive_time_period_milliseconds | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     if isinstance(value, dict) and CONF_CONDITION in value: | ||||
|         return schema(value) | ||||
|     return validate_wait_until({CONF_CONDITION: value}) | ||||
| _validate_wait_until = cv.maybe_simple_value( | ||||
|     { | ||||
|         cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|         cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds), | ||||
|     }, | ||||
|     key=CONF_CONDITION, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @register_action("wait_until", WaitUntilAction, validate_wait_until) | ||||
| @register_action("wait_until", WaitUntilAction, _validate_wait_until) | ||||
| async def wait_until_action_to_code(config, action_id, template_arg, args): | ||||
|     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, conditions) | ||||
|   | ||||
| @@ -63,6 +63,8 @@ from esphome.cpp_types import (  # noqa | ||||
|     uint32, | ||||
|     uint64, | ||||
|     int32, | ||||
|     int64, | ||||
|     size_t, | ||||
|     const_char_ptr, | ||||
|     NAN, | ||||
|     esphome_ns, | ||||
| @@ -75,11 +77,11 @@ from esphome.cpp_types import (  # noqa | ||||
|     optional, | ||||
|     arduino_json_ns, | ||||
|     JsonObject, | ||||
|     JsonObjectRef, | ||||
|     JsonObjectConstRef, | ||||
|     JsonObjectConst, | ||||
|     Controller, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     gpio_Flags, | ||||
|     EntityCategory, | ||||
|     Parented, | ||||
| ) | ||||
|   | ||||
| @@ -52,10 +52,10 @@ uint32_t IRAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { | ||||
|     this->gate_pin.digital_write(false); | ||||
|   } | ||||
|  | ||||
|   if (time_since_zc < this->enable_time_us) | ||||
|   if (time_since_zc < this->enable_time_us) { | ||||
|     // Next event is enable, return time until that event | ||||
|     return this->enable_time_us - time_since_zc; | ||||
|   else if (time_since_zc < disable_time_us) { | ||||
|   } else if (time_since_zc < disable_time_us) { | ||||
|     // Next event is disable, return time until that event | ||||
|     return this->disable_time_us - time_since_zc; | ||||
|   } | ||||
| @@ -74,9 +74,10 @@ uint32_t IRAM_ATTR HOT timer_interrupt() { | ||||
|   uint32_t min_dt_us = 1000; | ||||
|   uint32_t now = micros(); | ||||
|   for (auto *dimmer : all_dimmers) { | ||||
|     if (dimmer == nullptr) | ||||
|     if (dimmer == nullptr) { | ||||
|       // no more dimmers | ||||
|       break; | ||||
|     } | ||||
|     uint32_t res = dimmer->timer_intr(now); | ||||
|     if (res != 0 && res < min_dt_us) | ||||
|       min_dt_us = res; | ||||
| @@ -120,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|       // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic | ||||
|       // also take into account min_power | ||||
|       auto min_us = this->cycle_time_us * this->min_power / 1000; | ||||
|       this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); | ||||
|       // calculate required value to provide a true RMS voltage output | ||||
|       this->enable_time_us = | ||||
|           std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) * | ||||
|                                             (this->cycle_time_us - min_us)) / | ||||
|                                      65535); | ||||
|       if (this->method == DIM_METHOD_LEADING_PULSE) { | ||||
|         // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone | ||||
|         // this is for brightness near 99% | ||||
| @@ -212,12 +217,13 @@ void AcDimmer::dump_config() { | ||||
|   LOG_PIN("  Zero-Cross Pin: ", this->zero_cross_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "   Min Power: %.1f%%", this->store_.min_power / 10.0f); | ||||
|   ESP_LOGCONFIG(TAG, "   Init with half cycle: %s", YESNO(this->init_with_half_cycle_)); | ||||
|   if (method_ == DIM_METHOD_LEADING_PULSE) | ||||
|   if (method_ == DIM_METHOD_LEADING_PULSE) { | ||||
|     ESP_LOGCONFIG(TAG, "   Method: leading pulse"); | ||||
|   else if (method_ == DIM_METHOD_LEADING) | ||||
|   } else if (method_ == DIM_METHOD_LEADING) { | ||||
|     ESP_LOGCONFIG(TAG, "   Method: leading"); | ||||
|   else | ||||
|   } else { | ||||
|     ESP_LOGCONFIG(TAG, "   Method: trailing"); | ||||
|   } | ||||
|  | ||||
|   LOG_FLOAT_OUTPUT(this); | ||||
|   ESP_LOGV(TAG, "  Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); | ||||
|   | ||||
| @@ -13,7 +13,6 @@ class AdalightLightEffect : public light::AddressableLightEffect, public uart::U | ||||
|  public: | ||||
|   AdalightLightEffect(const std::string &name); | ||||
|  | ||||
|  public: | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   void apply(light::AddressableLight &it, const Color ¤t_color) override; | ||||
| @@ -30,7 +29,6 @@ class AdalightLightEffect : public light::AddressableLightEffect, public uart::U | ||||
|   void blank_all_leds_(light::AddressableLight &it); | ||||
|   Frame parse_frame_(light::AddressableLight &it); | ||||
|  | ||||
|  protected: | ||||
|   uint32_t last_ack_{0}; | ||||
|   uint32_t last_byte_{0}; | ||||
|   uint32_t last_reset_{0}; | ||||
|   | ||||
| @@ -16,6 +16,22 @@ namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
|  | ||||
| // 13bit for S2, and 12bit 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 int SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int 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 | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
| @@ -23,14 +39,14 @@ void ADCSensor::setup() { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc1_config_width(ADC_WIDTH_BIT_12); | ||||
|   adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|   if (!autorange_) { | ||||
|     adc1_config_channel_atten(channel_, attenuation_); | ||||
|   } | ||||
|  | ||||
|   // load characteristics for each attenuation | ||||
|   for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { | ||||
|     auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_BIT_12, | ||||
|     auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, | ||||
|                                               1100,  // default vref | ||||
|                                               &cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
| @@ -46,8 +62,8 @@ void ADCSensor::setup() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) | ||||
|   // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2 | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
| @@ -65,25 +81,26 @@ void ADCSensor::dump_config() { | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   LOG_PIN("  Pin: ", pin_); | ||||
|   if (autorange_) | ||||
|   if (autorange_) { | ||||
|     ESP_LOGCONFIG(TAG, " Attenuation: auto"); | ||||
|   else | ||||
|   } else { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_11: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| #endif  // USE_ESP32 | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
| @@ -123,16 +140,16 @@ float ADCSensor::sample() { | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); | ||||
|   raw11 = adc1_get_raw(channel_); | ||||
|   if (raw11 < 4095) { | ||||
|   if (raw11 < ADC_MAX) { | ||||
|     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); | ||||
|     raw6 = adc1_get_raw(channel_); | ||||
|     if (raw6 < 4095) { | ||||
|     if (raw6 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); | ||||
|       raw2 = adc1_get_raw(channel_); | ||||
|       if (raw2 < 4095) { | ||||
|       if (raw2 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); | ||||
|         raw0 = adc1_get_raw(channel_); | ||||
|       } | ||||
| @@ -148,15 +165,15 @@ float ADCSensor::sample() { | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   // Contribution of each value, in range 0-2048 | ||||
|   uint32_t c11 = std::min(raw11, 2048); | ||||
|   uint32_t c6 = 2048 - std::abs(raw6 - 2048); | ||||
|   uint32_t c2 = 2048 - std::abs(raw2 - 2048); | ||||
|   uint32_t c0 = std::min(4095 - raw0, 2048); | ||||
|   // max theoretical csum value is 2048*4 = 8192 | ||||
|   // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) | ||||
|   uint32_t c11 = std::min(raw11, 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 = c11 + c6 + c2 + c0; | ||||
|  | ||||
|   // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned | ||||
|   // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 | ||||
|   uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
|   | ||||
| @@ -133,6 +133,7 @@ ADCSensor = adc_ns.class_( | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     sensor.sensor_schema( | ||||
|         ADCSensor, | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=2, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
| @@ -140,7 +141,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADCSensor), | ||||
|             cv.Required(CONF_PIN): validate_adc_pin, | ||||
|             cv.Optional(CONF_RAW, default=False): cv.boolean, | ||||
|             cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( | ||||
|   | ||||
| @@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom | ||||
|   void setup() override; | ||||
|   void display(); | ||||
|  | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
|  | ||||
|  protected: | ||||
|   int get_width_internal() override; | ||||
|   int get_height_internal() override; | ||||
|   | ||||
| @@ -52,6 +52,7 @@ ADS1115Sensor = ads1115_ns.class_( | ||||
| CONF_ADS1115_ID = "ads1115_id" | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         ADS1115Sensor, | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=3, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
| @@ -59,7 +60,6 @@ CONFIG_SCHEMA = ( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADS1115Sensor), | ||||
|             cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), | ||||
|             cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), | ||||
|             cv.Required(CONF_GAIN): validate_gain, | ||||
|   | ||||
| @@ -24,7 +24,7 @@ void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
| @@ -56,7 +56,7 @@ void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto value = (WaveMiniReadings *) raw_value; | ||||
|   auto *value = (WaveMiniReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WaveMiniReadings) <= value_len) { | ||||
|     this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||
|   | ||||
| @@ -24,7 +24,7 @@ void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
| @@ -56,7 +56,7 @@ void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto value = (WavePlusReadings *) raw_value; | ||||
|   auto *value = (WavePlusReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WavePlusReadings) <= value_len) { | ||||
|     ESP_LOGD(TAG, "version = %d", value->version); | ||||
|   | ||||
| @@ -19,12 +19,14 @@ uint16_t crc_16(uint8_t *ptr, uint8_t length) { | ||||
|   //------------------------------ | ||||
|   while (length--) { | ||||
|     crc ^= *ptr++; | ||||
|     for (i = 0; i < 8; i++) | ||||
|     for (i = 0; i < 8; i++) { | ||||
|       if ((crc & 0x01) != 0) { | ||||
|         crc >>= 1; | ||||
|         crc ^= 0xA001; | ||||
|       } else | ||||
|       } else { | ||||
|         crc >>= 1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return crc; | ||||
| } | ||||
|   | ||||
| @@ -39,7 +39,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { | ||||
|           ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", | ||||
| @@ -75,13 +75,14 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i | ||||
|  | ||||
|       if (this->current_sensor_ > 0) { | ||||
|         if (this->illuminance_ != nullptr) { | ||||
|           auto packet = this->encoder_->get_light_level_request(); | ||||
|           auto *packet = this->encoder_->get_light_level_request(); | ||||
|           auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                                  packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                                  ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) | ||||
|           if (status) { | ||||
|             ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), | ||||
|                      status); | ||||
|           } | ||||
|         } | ||||
|         this->current_sensor_ = 0; | ||||
|       } | ||||
| @@ -99,7 +100,7 @@ void Am43::update() { | ||||
|   } | ||||
|   if (this->current_sensor_ == 0) { | ||||
|     if (this->battery_ != nullptr) { | ||||
|       auto packet = this->encoder_->get_battery_level_request(); | ||||
|       auto *packet = this->encoder_->get_battery_level_request(); | ||||
|       auto status = | ||||
|           esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                    packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from esphome.const import CONF_ID, CONF_PIN | ||||
|  | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| AUTO_LOAD = ["am43"] | ||||
| AUTO_LOAD = ["am43", "sensor"] | ||||
|  | ||||
| CONF_INVERT_POSITION = "invert_position" | ||||
|  | ||||
|   | ||||
| @@ -25,15 +25,16 @@ void Am43Component::setup() { | ||||
|  | ||||
| void Am43Component::loop() { | ||||
|   if (this->node_state == espbt::ClientState::ESTABLISHED && !this->logged_in_) { | ||||
|     auto packet = this->encoder_->get_send_pin_request(this->pin_); | ||||
|     auto *packet = this->encoder_->get_send_pin_request(this->pin_); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str()); | ||||
|     if (status) | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status); | ||||
|     else | ||||
|     } else { | ||||
|       this->logged_in_ = true; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -51,7 +52,7 @@ void Am43Component::control(const CoverCall &call) { | ||||
|     return; | ||||
|   } | ||||
|   if (call.get_stop()) { | ||||
|     auto packet = this->encoder_->get_stop_request(); | ||||
|     auto *packet = this->encoder_->get_stop_request(); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
| @@ -63,7 +64,7 @@ void Am43Component::control(const CoverCall &call) { | ||||
|  | ||||
|     if (this->invert_position_) | ||||
|       pos = 1 - pos; | ||||
|     auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); | ||||
|     auto *packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
| @@ -80,7 +81,7 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { | ||||
|           ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str()); | ||||
| @@ -120,7 +121,7 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ | ||||
|       if (this->decoder_->has_pin_response()) { | ||||
|         if (this->decoder_->pin_ok_) { | ||||
|           ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str()); | ||||
|           auto packet = this->encoder_->get_position_request(); | ||||
|           auto *packet = this->encoder_->get_position_request(); | ||||
|           auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                                  packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                                  ESP_GATT_AUTH_REQ_NONE); | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/analog_threshold/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/analog_threshold/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@ianchi"] | ||||
| @@ -0,0 +1,40 @@ | ||||
| #include "analog_threshold_binary_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace analog_threshold { | ||||
|  | ||||
| static const char *const TAG = "analog_threshold.binary_sensor"; | ||||
|  | ||||
| void AnalogThresholdBinarySensor::setup() { | ||||
|   float sensor_value = this->sensor_->get_state(); | ||||
|  | ||||
|   // TRUE state is defined to be when sensor is >= threshold | ||||
|   // so when undefined sensor value initialize to FALSE | ||||
|   if (std::isnan(sensor_value)) { | ||||
|     this->publish_initial_state(false); | ||||
|   } else { | ||||
|     this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) { | ||||
|   this->sensor_ = analog_sensor; | ||||
|  | ||||
|   this->sensor_->add_on_state_callback([this](float sensor_value) { | ||||
|     // if there is an invalid sensor reading, ignore the change and keep the current state | ||||
|     if (!std::isnan(sensor_value)) { | ||||
|       this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_)); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void AnalogThresholdBinarySensor::dump_config() { | ||||
|   LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this); | ||||
|   LOG_SENSOR("  ", "Sensor", this->sensor_); | ||||
|   ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_); | ||||
|   ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_); | ||||
| } | ||||
|  | ||||
| }  // namespace analog_threshold | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,29 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace analog_threshold { | ||||
|  | ||||
| class AnalogThresholdBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   void setup() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|   void set_sensor(sensor::Sensor *analog_sensor); | ||||
|   void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; } | ||||
|   void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; } | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *sensor_{nullptr}; | ||||
|  | ||||
|   float upper_threshold_; | ||||
|   float lower_threshold_; | ||||
| }; | ||||
|  | ||||
| }  // namespace analog_threshold | ||||
| }  // namespace esphome | ||||
							
								
								
									
										44
									
								
								esphome/components/analog_threshold/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/analog_threshold/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_SENSOR_ID, | ||||
|     CONF_THRESHOLD, | ||||
| ) | ||||
|  | ||||
| analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold") | ||||
|  | ||||
| AnalogThresholdBinarySensor = analog_threshold_ns.class_( | ||||
|     "AnalogThresholdBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
|  | ||||
| CONF_UPPER = "upper" | ||||
| CONF_LOWER = "lower" | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AnalogThresholdBinarySensor), | ||||
|         cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), | ||||
|         cv.Required(CONF_THRESHOLD): cv.Any( | ||||
|             cv.float_, | ||||
|             cv.Schema( | ||||
|                 {cv.Required(CONF_UPPER): cv.float_, cv.Required(CONF_LOWER): cv.float_} | ||||
|             ), | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     sens = await cg.get_variable(config[CONF_SENSOR_ID]) | ||||
|     cg.add(var.set_sensor(sens)) | ||||
|  | ||||
|     if isinstance(config[CONF_THRESHOLD], float): | ||||
|         cg.add(var.set_upper_threshold(config[CONF_THRESHOLD])) | ||||
|         cg.add(var.set_lower_threshold(config[CONF_THRESHOLD])) | ||||
|     else: | ||||
|         cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER])) | ||||
|         cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER])) | ||||
| @@ -94,6 +94,29 @@ async def to_code(config): | ||||
|                 data[pos] = pix[2] | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] == "RGB565": | ||||
|         data = [0 for _ in range(height * width * 2 * frames)] | ||||
|         pos = 0 | ||||
|         for frameIndex in range(frames): | ||||
|             image.seek(frameIndex) | ||||
|             frame = image.convert("RGB") | ||||
|             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: | ||||
|                 R = pix[0] >> 3 | ||||
|                 G = pix[1] >> 2 | ||||
|                 B = pix[2] >> 3 | ||||
|                 rgb = (R << 11) | (G << 5) | B | ||||
|                 data[pos] = rgb >> 8 | ||||
|                 pos += 1 | ||||
|                 data[pos] = rgb & 255 | ||||
|                 pos += 1 | ||||
|  | ||||
|     elif config[CONF_TYPE] == "BINARY": | ||||
|         width8 = ((width + 7) // 8) * 8 | ||||
|         data = [0 for _ in range((height * width8 // 8) * frames)] | ||||
|   | ||||
| @@ -40,7 +40,7 @@ void Anova::control(const ClimateCall &call) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|   } | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); | ||||
|     auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); | ||||
|     auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                            pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
| @@ -57,7 +57,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); | ||||
|       auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); | ||||
|         ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); | ||||
| @@ -114,9 +114,10 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ | ||||
|           auto status = | ||||
|               esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, | ||||
|                                        pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) | ||||
|           if (status) { | ||||
|             ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), | ||||
|                      status); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
| @@ -133,7 +134,7 @@ void Anova::update() { | ||||
|     return; | ||||
|  | ||||
|   if (this->current_request_ < 2) { | ||||
|     auto pkt = this->codec_->get_read_device_status_request(); | ||||
|     auto *pkt = this->codec_->get_read_device_status_request(); | ||||
|     if (this->current_request_ == 0) | ||||
|       this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); | ||||
|     auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|   | ||||
| @@ -36,7 +36,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode | ||||
|     traits.set_visual_temperature_step(0.1); | ||||
|     return traits; | ||||
|   } | ||||
|   void set_unit_of_measurement(const char *); | ||||
|   void set_unit_of_measurement(const char *unit); | ||||
|  | ||||
|  protected: | ||||
|   std::unique_ptr<AnovaCodec> codec_; | ||||
|   | ||||
| @@ -225,9 +225,10 @@ void APDS9960::read_gesture_data_() { | ||||
|  | ||||
|   uint8_t fifo_level; | ||||
|   APDS9960_WARNING_CHECK(this->read_byte(0xAE, &fifo_level), "Reading FIFO level failed."); | ||||
|   if (fifo_level == 0) | ||||
|   if (fifo_level == 0) { | ||||
|     // no data to process | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   APDS9960_WARNING_CHECK(fifo_level <= 32, "FIFO level has invalid value.") | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| from esphome.const import CONF_DIRECTION, CONF_DEVICE_CLASS, DEVICE_CLASS_MOVING | ||||
| from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING | ||||
| from . import APDS9960, CONF_APDS9960_ID | ||||
|  | ||||
| DEPENDENCIES = ["apds9960"] | ||||
| @@ -13,13 +13,12 @@ DIRECTIONS = { | ||||
|     "RIGHT": "set_right_direction", | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( | ||||
| CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( | ||||
|     device_class=DEVICE_CLASS_MOVING | ||||
| ).extend( | ||||
|     { | ||||
|         cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), | ||||
|         cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), | ||||
|         cv.Optional( | ||||
|             CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING | ||||
|         ): binary_sensor.device_class, | ||||
|         cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,8 @@ service APIConnection { | ||||
|   rpc number_command (NumberCommandRequest) returns (void) {} | ||||
|   rpc select_command (SelectCommandRequest) returns (void) {} | ||||
|   rpc button_command (ButtonCommandRequest) returns (void) {} | ||||
|   rpc lock_command (LockCommandRequest) returns (void) {} | ||||
|   rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -95,6 +97,9 @@ message HelloResponse { | ||||
|   // and only exists for debugging/logging purposes. | ||||
|   // For example "ESPHome v1.10.0 on ESP8266" | ||||
|   string server_info = 3; | ||||
|  | ||||
|   // The name of the server (App.get_name()) | ||||
|   string name = 4; | ||||
| } | ||||
|  | ||||
| // Message sent at the beginning of each connection to authenticate the client | ||||
| @@ -525,6 +530,7 @@ message ListEntitiesSwitchResponse { | ||||
|   bool assumed_state = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   string device_class = 9; | ||||
| } | ||||
| message SwitchStateResponse { | ||||
|   option (id) = 26; | ||||
| @@ -953,6 +959,63 @@ message SelectCommandRequest { | ||||
|   string state = 2; | ||||
| } | ||||
|  | ||||
|  | ||||
| // ==================== LOCK ==================== | ||||
| enum LockState { | ||||
|   LOCK_STATE_NONE = 0; | ||||
|   LOCK_STATE_LOCKED = 1; | ||||
|   LOCK_STATE_UNLOCKED = 2; | ||||
|   LOCK_STATE_JAMMED = 3; | ||||
|   LOCK_STATE_LOCKING = 4; | ||||
|   LOCK_STATE_UNLOCKING = 5; | ||||
| } | ||||
| enum LockCommand  { | ||||
|   LOCK_UNLOCK = 0; | ||||
|   LOCK_LOCK = 1; | ||||
|   LOCK_OPEN = 2; | ||||
| } | ||||
| message ListEntitiesLockResponse { | ||||
|   option (id) = 58; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LOCK"; | ||||
|  | ||||
|   string object_id = 1; | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|   bool assumed_state = 8; | ||||
|  | ||||
|   bool supports_open = 9; | ||||
|   bool requires_code = 10; | ||||
|  | ||||
|   // Not yet implemented: | ||||
|   string code_format = 11; | ||||
| } | ||||
| message LockStateResponse { | ||||
|   option (id) = 59; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LOCK"; | ||||
|   option (no_delay) = true; | ||||
|   fixed32 key = 1; | ||||
|   LockState state = 2; | ||||
| } | ||||
| message LockCommandRequest { | ||||
|   option (id) = 60; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_LOCK"; | ||||
|   option (no_delay) = true; | ||||
|   fixed32 key = 1; | ||||
|   LockCommand command = 2; | ||||
|  | ||||
|   // Not yet implemented: | ||||
|   bool has_code = 3; | ||||
|   string code = 4; | ||||
| } | ||||
|  | ||||
| // ==================== BUTTON ==================== | ||||
| message ListEntitiesButtonResponse { | ||||
|   option (id) = 61; | ||||
| @@ -977,3 +1040,61 @@ message ButtonCommandRequest { | ||||
|  | ||||
|   fixed32 key = 1; | ||||
| } | ||||
|  | ||||
| // ==================== MEDIA PLAYER ==================== | ||||
| enum MediaPlayerState { | ||||
|   MEDIA_PLAYER_STATE_NONE = 0; | ||||
|   MEDIA_PLAYER_STATE_IDLE = 1; | ||||
|   MEDIA_PLAYER_STATE_PLAYING = 2; | ||||
|   MEDIA_PLAYER_STATE_PAUSED = 3; | ||||
| } | ||||
| enum MediaPlayerCommand { | ||||
|   MEDIA_PLAYER_COMMAND_PLAY = 0; | ||||
|   MEDIA_PLAYER_COMMAND_PAUSE = 1; | ||||
|   MEDIA_PLAYER_COMMAND_STOP = 2; | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3; | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4; | ||||
| } | ||||
| message ListEntitiesMediaPlayerResponse { | ||||
|   option (id) = 63; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|  | ||||
|   string object_id = 1; | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|  | ||||
|   bool supports_pause = 8; | ||||
| } | ||||
| message MediaPlayerStateResponse { | ||||
|   option (id) = 64; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|   option (no_delay) = true; | ||||
|   fixed32 key = 1; | ||||
|   MediaPlayerState state = 2; | ||||
|   float volume = 3; | ||||
|   bool muted = 4; | ||||
| } | ||||
| message MediaPlayerCommandRequest { | ||||
|   option (id) = 65; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|  | ||||
|   bool has_command = 2; | ||||
|   MediaPlayerCommand command = 3; | ||||
|  | ||||
|   bool has_volume = 4; | ||||
|   float volume = 5; | ||||
|  | ||||
|   bool has_media_url = 6; | ||||
|   string media_url = 7; | ||||
| } | ||||
|   | ||||
| @@ -12,17 +12,15 @@ | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
| #include "esphome/components/homeassistant/time/homeassistant_time.h" | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| #include "esphome/components/fan/fan_helpers.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.connection"; | ||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||
|  | ||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||
|     : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { | ||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||
|   this->proto_write_buffer_.reserve(64); | ||||
|  | ||||
| #if defined(USE_API_PLAINTEXT) | ||||
| @@ -104,6 +102,7 @@ void APIConnection::loop() { | ||||
|       ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); | ||||
|     } | ||||
|   } else if (now - this->last_traffic_ > keepalive) { | ||||
|     ESP_LOGVV(TAG, "Sending keepalive PING..."); | ||||
|     this->sent_ping_ = true; | ||||
|     this->send_ping_request(PingRequest()); | ||||
|   } | ||||
| @@ -251,10 +250,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
| // Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
| bool APIConnection::send_fan_state(fan::FanState *fan) { | ||||
| bool APIConnection::send_fan_state(fan::Fan *fan) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
| @@ -266,13 +262,12 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { | ||||
|     resp.oscillating = fan->oscillating; | ||||
|   if (traits.supports_speed()) { | ||||
|     resp.speed_level = fan->speed; | ||||
|     resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); | ||||
|   } | ||||
|   if (traits.supports_direction()) | ||||
|     resp.direction = static_cast<enums::FanDirection>(fan->direction); | ||||
|   return this->send_fan_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_fan_info(fan::FanState *fan) { | ||||
| bool APIConnection::send_fan_info(fan::Fan *fan) { | ||||
|   auto traits = fan->get_traits(); | ||||
|   ListEntitiesFanResponse msg; | ||||
|   msg.key = fan->get_object_id_hash(); | ||||
| @@ -289,12 +284,10 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { | ||||
|   return this->send_list_entities_fan_response(msg); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|   fan::FanState *fan = App.get_fan_by_key(msg.key); | ||||
|   fan::Fan *fan = App.get_fan_by_key(msg.key); | ||||
|   if (fan == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto traits = fan->get_traits(); | ||||
|  | ||||
|   auto call = fan->make_call(); | ||||
|   if (msg.has_state) | ||||
|     call.set_state(msg.state); | ||||
| @@ -303,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|   if (msg.has_speed_level) { | ||||
|     // Prefer level | ||||
|     call.set_speed(msg.speed_level); | ||||
|   } else if (msg.has_speed) { | ||||
|     call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count())); | ||||
|   } | ||||
|   if (msg.has_direction) | ||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||
|   call.perform(); | ||||
| } | ||||
| #pragma GCC diagnostic pop | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
| @@ -461,6 +451,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
|   msg.assumed_state = a_switch->assumed_state(); | ||||
|   msg.disabled_by_default = a_switch->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(a_switch->get_entity_category()); | ||||
|   msg.device_class = a_switch->get_device_class(); | ||||
|   return this->send_list_entities_switch_response(msg); | ||||
| } | ||||
| void APIConnection::switch_command(const SwitchCommandRequest &msg) { | ||||
| @@ -468,10 +459,11 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) { | ||||
|   if (a_switch == nullptr) | ||||
|     return; | ||||
|  | ||||
|   if (msg.state) | ||||
|   if (msg.state) { | ||||
|     a_switch->turn_on(); | ||||
|   else | ||||
|   } else { | ||||
|     a_switch->turn_off(); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -698,13 +690,104 @@ void APIConnection::button_command(const ButtonCommandRequest &msg) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
| bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   LockStateResponse resp{}; | ||||
|   resp.key = a_lock->get_object_id_hash(); | ||||
|   resp.state = static_cast<enums::LockState>(state); | ||||
|   return this->send_lock_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_lock_info(lock::Lock *a_lock) { | ||||
|   ListEntitiesLockResponse msg; | ||||
|   msg.key = a_lock->get_object_id_hash(); | ||||
|   msg.object_id = a_lock->get_object_id(); | ||||
|   msg.name = a_lock->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("lock", a_lock); | ||||
|   msg.icon = a_lock->get_icon(); | ||||
|   msg.assumed_state = a_lock->traits.get_assumed_state(); | ||||
|   msg.disabled_by_default = a_lock->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(a_lock->get_entity_category()); | ||||
|   msg.supports_open = a_lock->traits.get_supports_open(); | ||||
|   msg.requires_code = a_lock->traits.get_requires_code(); | ||||
|   return this->send_list_entities_lock_response(msg); | ||||
| } | ||||
| void APIConnection::lock_command(const LockCommandRequest &msg) { | ||||
|   lock::Lock *a_lock = App.get_lock_by_key(msg.key); | ||||
|   if (a_lock == nullptr) | ||||
|     return; | ||||
|  | ||||
|   switch (msg.command) { | ||||
|     case enums::LOCK_UNLOCK: | ||||
|       a_lock->unlock(); | ||||
|       break; | ||||
|     case enums::LOCK_LOCK: | ||||
|       a_lock->lock(); | ||||
|       break; | ||||
|     case enums::LOCK_OPEN: | ||||
|       a_lock->open(); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   MediaPlayerStateResponse resp{}; | ||||
|   resp.key = media_player->get_object_id_hash(); | ||||
|   resp.state = static_cast<enums::MediaPlayerState>(media_player->state); | ||||
|   resp.volume = media_player->volume; | ||||
|   resp.muted = media_player->is_muted(); | ||||
|   return this->send_media_player_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { | ||||
|   ListEntitiesMediaPlayerResponse msg; | ||||
|   msg.key = media_player->get_object_id_hash(); | ||||
|   msg.object_id = media_player->get_object_id(); | ||||
|   msg.name = media_player->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("media_player", media_player); | ||||
|   msg.icon = media_player->get_icon(); | ||||
|   msg.disabled_by_default = media_player->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category()); | ||||
|  | ||||
|   auto traits = media_player->get_traits(); | ||||
|   msg.supports_pause = traits.get_supports_pause(); | ||||
|  | ||||
|   return this->send_list_entities_media_player_response(msg); | ||||
| } | ||||
| void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { | ||||
|   media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); | ||||
|   if (media_player == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto call = media_player->make_call(); | ||||
|   if (msg.has_command) { | ||||
|     call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command)); | ||||
|   } | ||||
|   if (msg.has_volume) { | ||||
|     call.set_volume(msg.volume); | ||||
|   } | ||||
|   if (msg.has_media_url) { | ||||
|     call.set_media_url(msg.media_url); | ||||
|   } | ||||
|   call.perform(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
|   if (!this->state_subscription_) | ||||
|     return; | ||||
|   if (this->image_reader_.available()) | ||||
|     return; | ||||
|   this->image_reader_.set_image(std::move(image)); | ||||
|   if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) || | ||||
|       image->was_requested_by(esphome::esp32_camera::IDLE)) | ||||
|     this->image_reader_.set_image(std::move(image)); | ||||
| } | ||||
| bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   ListEntitiesCameraResponse msg; | ||||
| @@ -722,9 +805,14 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { | ||||
|     return; | ||||
|  | ||||
|   if (msg.single) | ||||
|     esp32_camera::global_esp32_camera->request_image(); | ||||
|   if (msg.stream) | ||||
|     esp32_camera::global_esp32_camera->request_stream(); | ||||
|     esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER); | ||||
|   if (msg.stream) { | ||||
|     esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER); | ||||
|  | ||||
|     App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() { | ||||
|       esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -758,6 +846,8 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 6; | ||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   resp.name = App.get_name(); | ||||
|  | ||||
|   this->connection_state_ = ConnectionState::CONNECTED; | ||||
|   return resp; | ||||
| } | ||||
| @@ -795,15 +885,16 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
|   resp.project_version = ESPHOME_PROJECT_VERSION; | ||||
| #endif | ||||
| #ifdef USE_WEBSERVER | ||||
|   resp.webserver_port = WEBSERVER_PORT; | ||||
|   resp.webserver_port = USE_WEBSERVER_PORT; | ||||
| #endif | ||||
|   return resp; | ||||
| } | ||||
| void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { | ||||
|   for (auto &it : this->parent_->get_state_subs()) | ||||
|   for (auto &it : this->parent_->get_state_subs()) { | ||||
|     if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { | ||||
|       it.callback(msg.state); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|   bool found = false; | ||||
| @@ -852,7 +943,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|   this->last_traffic_ = millis(); | ||||
|   // Do not set last_traffic_ on send | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   | ||||
| @@ -32,8 +32,8 @@ class APIConnection : public APIServerConnection { | ||||
|   void cover_command(const CoverCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   bool send_fan_state(fan::FanState *fan); | ||||
|   bool send_fan_info(fan::FanState *fan); | ||||
|   bool send_fan_state(fan::Fan *fan); | ||||
|   bool send_fan_info(fan::Fan *fan); | ||||
|   void fan_command(const FanCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| @@ -77,6 +77,16 @@ class APIConnection : public APIServerConnection { | ||||
| #ifdef USE_BUTTON | ||||
|   bool send_button_info(button::Button *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 lock_command(const LockCommandRequest &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 media_player_command(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool send_log_message(int level, const char *tag, const char *line); | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| #include "api_frame_helper.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "proto.h" | ||||
| #include <cstring> | ||||
|  | ||||
| @@ -252,7 +254,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); | ||||
| #endif | ||||
|   frame->msg = std::move(rx_buf_); | ||||
|   // consume msg | ||||
| @@ -301,9 +303,16 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|   } | ||||
|   if (state_ == State::SERVER_HELLO) { | ||||
|     // send server hello | ||||
|     uint8_t msg[1]; | ||||
|     msg[0] = 0x01;  // chosen proto | ||||
|     aerr = write_frame_(msg, 1); | ||||
|     std::vector<uint8_t> msg; | ||||
|     // chosen proto | ||||
|     msg.push_back(0x01); | ||||
|  | ||||
|     // node name, terminated by null byte | ||||
|     const std::string &name = App.get_name(); | ||||
|     const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str()); | ||||
|     msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); | ||||
|  | ||||
|     aerr = write_frame_(msg.data(), msg.size()); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
| @@ -546,7 +555,8 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { | ||||
|   size_t total_write_len = 0; | ||||
|   for (int i = 0; i < iovcnt; i++) { | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", | ||||
|               format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
| #endif | ||||
|     total_write_len += iov[i].iov_len; | ||||
|   } | ||||
| @@ -720,7 +730,12 @@ APIError APINoiseFrameHelper::shutdown(int how) { | ||||
| } | ||||
| extern "C" { | ||||
| // declare how noise generates random bytes (here with a good HWRNG based on the RF system) | ||||
| void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); } | ||||
| void noise_rand_bytes(void *output, size_t len) { | ||||
|   if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) { | ||||
|     ESP_LOGE(TAG, "Failed to acquire random bytes, rebooting!"); | ||||
|     arch_restart(); | ||||
|   } | ||||
| } | ||||
| } | ||||
| #endif  // USE_API_NOISE | ||||
|  | ||||
| @@ -855,7 +870,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); | ||||
| #endif | ||||
|   frame->msg = std::move(rx_buf_); | ||||
|   // consume msg | ||||
| @@ -934,7 +949,8 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt | ||||
|   size_t total_write_len = 0; | ||||
|   for (int i = 0; i < iovcnt; i++) { | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", | ||||
|               format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
| #endif | ||||
|     total_write_len += iov[i].iov_len; | ||||
|   } | ||||
|   | ||||
| @@ -278,6 +278,66 @@ template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState value) { | ||||
|   switch (value) { | ||||
|     case enums::LOCK_STATE_NONE: | ||||
|       return "LOCK_STATE_NONE"; | ||||
|     case enums::LOCK_STATE_LOCKED: | ||||
|       return "LOCK_STATE_LOCKED"; | ||||
|     case enums::LOCK_STATE_UNLOCKED: | ||||
|       return "LOCK_STATE_UNLOCKED"; | ||||
|     case enums::LOCK_STATE_JAMMED: | ||||
|       return "LOCK_STATE_JAMMED"; | ||||
|     case enums::LOCK_STATE_LOCKING: | ||||
|       return "LOCK_STATE_LOCKING"; | ||||
|     case enums::LOCK_STATE_UNLOCKING: | ||||
|       return "LOCK_STATE_UNLOCKING"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockCommand value) { | ||||
|   switch (value) { | ||||
|     case enums::LOCK_UNLOCK: | ||||
|       return "LOCK_UNLOCK"; | ||||
|     case enums::LOCK_LOCK: | ||||
|       return "LOCK_LOCK"; | ||||
|     case enums::LOCK_OPEN: | ||||
|       return "LOCK_OPEN"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) { | ||||
|   switch (value) { | ||||
|     case enums::MEDIA_PLAYER_STATE_NONE: | ||||
|       return "MEDIA_PLAYER_STATE_NONE"; | ||||
|     case enums::MEDIA_PLAYER_STATE_IDLE: | ||||
|       return "MEDIA_PLAYER_STATE_IDLE"; | ||||
|     case enums::MEDIA_PLAYER_STATE_PLAYING: | ||||
|       return "MEDIA_PLAYER_STATE_PLAYING"; | ||||
|     case enums::MEDIA_PLAYER_STATE_PAUSED: | ||||
|       return "MEDIA_PLAYER_STATE_PAUSED"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) { | ||||
|   switch (value) { | ||||
|     case enums::MEDIA_PLAYER_COMMAND_PLAY: | ||||
|       return "MEDIA_PLAYER_COMMAND_PLAY"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_PAUSE: | ||||
|       return "MEDIA_PLAYER_COMMAND_PAUSE"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_STOP: | ||||
|       return "MEDIA_PLAYER_COMMAND_STOP"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_MUTE: | ||||
|       return "MEDIA_PLAYER_COMMAND_MUTE"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_UNMUTE: | ||||
|       return "MEDIA_PLAYER_COMMAND_UNMUTE"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
| @@ -319,6 +379,10 @@ bool HelloResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) | ||||
|       this->server_info = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -327,6 +391,7 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(1, this->api_version_major); | ||||
|   buffer.encode_uint32(2, this->api_version_minor); | ||||
|   buffer.encode_string(3, this->server_info); | ||||
|   buffer.encode_string(4, this->name); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void HelloResponse::dump_to(std::string &out) const { | ||||
| @@ -345,6 +410,10 @@ void HelloResponse::dump_to(std::string &out) const { | ||||
|   out.append("  server_info: "); | ||||
|   out.append("'").append(this->server_info).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2138,6 +2207,10 @@ bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDel | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->device_class = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2161,6 +2234,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->assumed_state); | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(8, this->entity_category); | ||||
|   buffer.encode_string(9, this->device_class); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
| @@ -2198,6 +2272,10 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  device_class: "); | ||||
|   out.append("'").append(this->device_class).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -4177,6 +4255,234 @@ void SelectCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->assumed_state = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->supports_open = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->requires_code = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesLockResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->object_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->unique_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->code_format = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesLockResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->object_id); | ||||
|   buffer.encode_fixed32(2, this->key); | ||||
|   buffer.encode_string(3, this->name); | ||||
|   buffer.encode_string(4, this->unique_id); | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_bool(8, this->assumed_state); | ||||
|   buffer.encode_bool(9, this->supports_open); | ||||
|   buffer.encode_bool(10, this->requires_code); | ||||
|   buffer.encode_string(11, this->code_format); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesLockResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ListEntitiesLockResponse {\n"); | ||||
|   out.append("  object_id: "); | ||||
|   out.append("'").append(this->object_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  unique_id: "); | ||||
|   out.append("'").append(this->unique_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  assumed_state: "); | ||||
|   out.append(YESNO(this->assumed_state)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  supports_open: "); | ||||
|   out.append(YESNO(this->supports_open)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  requires_code: "); | ||||
|   out.append(YESNO(this->requires_code)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  code_format: "); | ||||
|   out.append("'").append(this->code_format).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->state = value.as_enum<enums::LockState>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void LockStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_enum<enums::LockState>(2, this->state); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void LockStateResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("LockStateResponse {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  state: "); | ||||
|   out.append(proto_enum_to_string<enums::LockState>(this->state)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->command = value.as_enum<enums::LockCommand>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->has_code = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 4: { | ||||
|       this->code = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void LockCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_enum<enums::LockCommand>(2, this->command); | ||||
|   buffer.encode_bool(3, this->has_code); | ||||
|   buffer.encode_string(4, this->code); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void LockCommandRequest::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("LockCommandRequest {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  command: "); | ||||
|   out.append(proto_enum_to_string<enums::LockCommand>(this->command)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_code: "); | ||||
|   out.append(YESNO(this->has_code)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  code: "); | ||||
|   out.append("'").append(this->code).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
| @@ -4239,7 +4545,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesButtonResponse::dump_to(std::string &out) const { | ||||
|   char buffer[64]; | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ListEntitiesButtonResponse {\n"); | ||||
|   out.append("  object_id: "); | ||||
|   out.append("'").append(this->object_id).append("'"); | ||||
| @@ -4289,7 +4595,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
| void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ButtonCommandRequest::dump_to(std::string &out) const { | ||||
|   char buffer[64]; | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ButtonCommandRequest {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
| @@ -4298,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->supports_pause = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->object_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->unique_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->object_id); | ||||
|   buffer.encode_fixed32(2, this->key); | ||||
|   buffer.encode_string(3, this->name); | ||||
|   buffer.encode_string(4, this->unique_id); | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_bool(8, this->supports_pause); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ListEntitiesMediaPlayerResponse {\n"); | ||||
|   out.append("  object_id: "); | ||||
|   out.append("'").append(this->object_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  unique_id: "); | ||||
|   out.append("'").append(this->unique_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  supports_pause: "); | ||||
|   out.append(YESNO(this->supports_pause)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->state = value.as_enum<enums::MediaPlayerState>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->muted = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->volume = value.as_float(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_enum<enums::MediaPlayerState>(2, this->state); | ||||
|   buffer.encode_float(3, this->volume); | ||||
|   buffer.encode_bool(4, this->muted); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void MediaPlayerStateResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("MediaPlayerStateResponse {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  state: "); | ||||
|   out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  volume: "); | ||||
|   sprintf(buffer, "%g", this->volume); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  muted: "); | ||||
|   out.append(YESNO(this->muted)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->has_command = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->command = value.as_enum<enums::MediaPlayerCommand>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->has_volume = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 6: { | ||||
|       this->has_media_url = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 7: { | ||||
|       this->media_url = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->volume = value.as_float(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_bool(2, this->has_command); | ||||
|   buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command); | ||||
|   buffer.encode_bool(4, this->has_volume); | ||||
|   buffer.encode_float(5, this->volume); | ||||
|   buffer.encode_bool(6, this->has_media_url); | ||||
|   buffer.encode_string(7, this->media_url); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void MediaPlayerCommandRequest::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("MediaPlayerCommandRequest {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_command: "); | ||||
|   out.append(YESNO(this->has_command)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  command: "); | ||||
|   out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_volume: "); | ||||
|   out.append(YESNO(this->has_volume)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  volume: "); | ||||
|   sprintf(buffer, "%g", this->volume); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_media_url: "); | ||||
|   out.append(YESNO(this->has_media_url)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  media_url: "); | ||||
|   out.append("'").append(this->media_url).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -128,6 +128,32 @@ enum NumberMode : uint32_t { | ||||
|   NUMBER_MODE_BOX = 1, | ||||
|   NUMBER_MODE_SLIDER = 2, | ||||
| }; | ||||
| enum LockState : uint32_t { | ||||
|   LOCK_STATE_NONE = 0, | ||||
|   LOCK_STATE_LOCKED = 1, | ||||
|   LOCK_STATE_UNLOCKED = 2, | ||||
|   LOCK_STATE_JAMMED = 3, | ||||
|   LOCK_STATE_LOCKING = 4, | ||||
|   LOCK_STATE_UNLOCKING = 5, | ||||
| }; | ||||
| enum LockCommand : uint32_t { | ||||
|   LOCK_UNLOCK = 0, | ||||
|   LOCK_LOCK = 1, | ||||
|   LOCK_OPEN = 2, | ||||
| }; | ||||
| enum MediaPlayerState : uint32_t { | ||||
|   MEDIA_PLAYER_STATE_NONE = 0, | ||||
|   MEDIA_PLAYER_STATE_IDLE = 1, | ||||
|   MEDIA_PLAYER_STATE_PLAYING = 2, | ||||
|   MEDIA_PLAYER_STATE_PAUSED = 3, | ||||
| }; | ||||
| enum MediaPlayerCommand : uint32_t { | ||||
|   MEDIA_PLAYER_COMMAND_PLAY = 0, | ||||
|   MEDIA_PLAYER_COMMAND_PAUSE = 1, | ||||
|   MEDIA_PLAYER_COMMAND_STOP = 2, | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3, | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4, | ||||
| }; | ||||
|  | ||||
| }  // namespace enums | ||||
|  | ||||
| @@ -147,6 +173,7 @@ class HelloResponse : public ProtoMessage { | ||||
|   uint32_t api_version_major{0}; | ||||
|   uint32_t api_version_minor{0}; | ||||
|   std::string server_info{}; | ||||
|   std::string name{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -566,6 +593,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { | ||||
|   bool assumed_state{false}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -1048,6 +1076,58 @@ class SelectCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesLockResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_open{false}; | ||||
|   bool requires_code{false}; | ||||
|   std::string code_format{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class LockStateResponse : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   enums::LockState state{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class LockCommandRequest : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   enums::LockCommand command{}; | ||||
|   bool has_code{false}; | ||||
|   std::string code{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesButtonResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
| @@ -1079,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_pause{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class MediaPlayerStateResponse : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   enums::MediaPlayerState state{}; | ||||
|   float volume{0.0f}; | ||||
|   bool muted{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class MediaPlayerCommandRequest : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   bool has_command{false}; | ||||
|   enums::MediaPlayerCommand command{}; | ||||
|   bool has_volume{false}; | ||||
|   float volume{0.0f}; | ||||
|   bool has_media_url{false}; | ||||
|   std::string media_url{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -282,6 +282,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_list_entities_lock_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<ListEntitiesLockResponse>(msg, 58); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| bool APIServerConnectionBase::send_lock_state_response(const LockStateResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_lock_state_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<LockStateResponse>(msg, 59); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| bool APIServerConnectionBase::send_list_entities_button_response(const ListEntitiesButtonResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -292,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<MediaPlayerStateResponse>(msg, 64); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| #endif | ||||
| bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | ||||
|   switch (msg_type) { | ||||
|     case 1: { | ||||
| @@ -523,6 +559,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_select_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 60: { | ||||
| #ifdef USE_LOCK | ||||
|       LockCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_lock_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
| @@ -534,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_button_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 65: { | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|       MediaPlayerCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_media_player_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
| @@ -771,6 +829,32 @@ void APIServerConnection::on_button_command_request(const ButtonCommandRequest & | ||||
|   this->button_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->lock_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->media_player_command(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -130,11 +130,29 @@ class APIServerConnectionBase : public ProtoService { | ||||
| #ifdef USE_SELECT | ||||
|   virtual void on_select_command_request(const SelectCommandRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool send_lock_state_response(const LockStateResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   virtual void on_lock_command_request(const LockCommandRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   bool send_list_entities_button_response(const ListEntitiesButtonResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   virtual void on_button_command_request(const ButtonCommandRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_media_player_state_response(const MediaPlayerStateResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; | ||||
| #endif | ||||
|  protected: | ||||
|   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; | ||||
| @@ -180,6 +198,12 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   virtual void button_command(const ButtonCommandRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   virtual void lock_command(const LockCommandRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; | ||||
| #endif | ||||
|  protected: | ||||
|   void on_hello_request(const HelloRequest &msg) override; | ||||
| @@ -221,6 +245,12 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_BUTTON | ||||
|   void on_button_command_request(const ButtonCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   void on_lock_command_request(const LockCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
| @@ -24,7 +24,7 @@ static const char *const TAG = "api"; | ||||
| void APIServer::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); | ||||
|   this->setup_controller(); | ||||
|   socket_ = socket::socket(AF_INET, SOCK_STREAM, 0); | ||||
|   socket_ = socket::socket_ip(SOCK_STREAM, 0); | ||||
|   if (socket_ == nullptr) { | ||||
|     ESP_LOGW(TAG, "Could not create socket."); | ||||
|     this->mark_failed(); | ||||
| @@ -43,13 +43,16 @@ void APIServer::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   struct sockaddr_in server; | ||||
|   memset(&server, 0, sizeof(server)); | ||||
|   server.sin_family = AF_INET; | ||||
|   server.sin_addr.s_addr = ESPHOME_INADDR_ANY; | ||||
|   server.sin_port = htons(this->port_); | ||||
|   struct sockaddr_storage server; | ||||
|  | ||||
|   err = socket_->bind((struct sockaddr *) &server, sizeof(server)); | ||||
|   socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), htons(this->port_)); | ||||
|   if (sl == 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   err = socket_->bind((struct sockaddr *) &server, sl); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); | ||||
|     this->mark_failed(); | ||||
| @@ -80,9 +83,10 @@ void APIServer::setup() { | ||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||
|     esp32_camera::global_esp32_camera->add_image_callback( | ||||
|         [this](const std::shared_ptr<esp32_camera::CameraImage> &image) { | ||||
|           for (auto &c : this->clients_) | ||||
|           for (auto &c : this->clients_) { | ||||
|             if (!c->remove_) | ||||
|               c->send_camera_state(image); | ||||
|           } | ||||
|         }); | ||||
|   } | ||||
| #endif | ||||
| @@ -188,7 +192,7 @@ void APIServer::on_cover_update(cover::Cover *obj) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
| void APIServer::on_fan_update(fan::FanState *obj) { | ||||
| void APIServer::on_fan_update(fan::Fan *obj) { | ||||
|   if (obj->is_internal()) | ||||
|     return; | ||||
|   for (auto &c : this->clients_) | ||||
| @@ -251,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
| void APIServer::on_select_update(select::Select *obj, const std::string &state) { | ||||
| void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { | ||||
|   if (obj->is_internal()) | ||||
|     return; | ||||
|   for (auto &c : this->clients_) | ||||
| @@ -259,6 +263,24 @@ void APIServer::on_select_update(select::Select *obj, const std::string &state) | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
| void APIServer::on_lock_update(lock::Lock *obj) { | ||||
|   if (obj->is_internal()) | ||||
|     return; | ||||
|   for (auto &c : this->clients_) | ||||
|     c->send_lock_state(obj, obj->state); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { | ||||
|   if (obj->is_internal()) | ||||
|     return; | ||||
|   for (auto &c : this->clients_) | ||||
|     c->send_media_player_state(obj); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } | ||||
| void APIServer::set_port(uint16_t port) { this->port_ = port; } | ||||
| APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
| #include "esphome/components/socket/socket.h" | ||||
| #include "api_pb2.h" | ||||
| #include "api_pb2_service.h" | ||||
| #include "util.h" | ||||
| #include "list_entities.h" | ||||
| #include "subscribe_state.h" | ||||
| #include "user_services.h" | ||||
| @@ -44,7 +43,7 @@ class APIServer : public Component, public Controller { | ||||
|   void on_cover_update(cover::Cover *obj) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   void on_fan_update(fan::FanState *obj) override; | ||||
|   void on_fan_update(fan::Fan *obj) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|   void on_light_update(light::LightState *obj) override; | ||||
| @@ -65,7 +64,13 @@ class APIServer : public Component, public Controller { | ||||
|   void on_number_update(number::Number *obj, float state) override; | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   void on_select_update(select::Select *obj, const std::string &state) override; | ||||
|   void on_select_update(select::Select *obj, const std::string &state, size_t index) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   void on_lock_update(lock::Lock *obj) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
|   | ||||
| @@ -21,7 +21,6 @@ async def async_run_logs(config, address): | ||||
|     if CONF_ENCRYPTION in conf: | ||||
|         noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] | ||||
|     _LOGGER.info("Starting log output from %s using esphome API", address) | ||||
|     zc = zeroconf.Zeroconf() | ||||
|     cli = APIClient( | ||||
|         address, | ||||
|         port, | ||||
|   | ||||
| @@ -12,10 +12,10 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s | ||||
|  public: | ||||
|   TemplatableStringValue() : TemplatableValue<std::string, X...>() {} | ||||
|  | ||||
|   template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0> | ||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> | ||||
|   TemplatableStringValue(F value) : TemplatableValue<std::string, X...>(value) {} | ||||
|  | ||||
|   template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0> | ||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> | ||||
|   TemplatableStringValue(F f) | ||||
|       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} | ||||
| }; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_ | ||||
| bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| bool ListEntitiesIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_info(fan); } | ||||
| bool ListEntitiesIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_info(fan); } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); } | ||||
| @@ -35,10 +35,12 @@ bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) | ||||
|   return this->client_->send_text_sensor_info(text_sensor); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); } | ||||
| #endif | ||||
|  | ||||
| bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } | ||||
| ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) | ||||
|     : ComponentIterator(server), client_(client) {} | ||||
| ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} | ||||
| bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
|   auto resp = service->encode_list_service_response(); | ||||
|   return this->client_->send_list_entities_services_response(resp); | ||||
| @@ -62,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_info(media_player); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "util.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| @@ -11,7 +11,7 @@ class APIConnection; | ||||
|  | ||||
| class ListEntitiesIterator : public ComponentIterator { | ||||
|  public: | ||||
|   ListEntitiesIterator(APIServer *server, APIConnection *client); | ||||
|   ListEntitiesIterator(APIConnection *client); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; | ||||
| #endif | ||||
| @@ -19,7 +19,7 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|   bool on_cover(cover::Cover *cover) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   bool on_fan(fan::FanState *fan) override; | ||||
|   bool on_fan(fan::Fan *fan) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|   bool on_light(light::LightState *light) override; | ||||
| @@ -48,6 +48,12 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   bool on_select(select::Select *select) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool on_lock(lock::Lock *a_lock) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||
| #endif | ||||
|   bool on_end() override; | ||||
|  | ||||
| @@ -57,5 +63,3 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|  | ||||
| #include "api_server.h" | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| #include "proto.h" | ||||
| #include "util.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
|   | ||||
| @@ -55,17 +55,19 @@ class ProtoVarInt { | ||||
|   } | ||||
|   int32_t as_sint32() const { | ||||
|     // with ZigZag encoding | ||||
|     if (this->value_ & 1) | ||||
|     if (this->value_ & 1) { | ||||
|       return static_cast<int32_t>(~(this->value_ >> 1)); | ||||
|     else | ||||
|     } else { | ||||
|       return static_cast<int32_t>(this->value_ >> 1); | ||||
|     } | ||||
|   } | ||||
|   int64_t as_sint64() const { | ||||
|     // with ZigZag encoding | ||||
|     if (this->value_ & 1) | ||||
|     if (this->value_ & 1) { | ||||
|       return static_cast<int64_t>(~(this->value_ >> 1)); | ||||
|     else | ||||
|     } else { | ||||
|       return static_cast<int64_t>(this->value_ >> 1); | ||||
|     } | ||||
|   } | ||||
|   void encode(std::vector<uint8_t> &out) { | ||||
|     uint32_t val = this->value_; | ||||
| @@ -193,6 +195,20 @@ class ProtoWriteBuffer { | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|   } | ||||
|   void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { | ||||
|     if (value == 0 && !force) | ||||
|       return; | ||||
|  | ||||
|     this->encode_field_raw(field_id, 5); | ||||
|     this->write((value >> 0) & 0xFF); | ||||
|     this->write((value >> 8) & 0xFF); | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|     this->write((value >> 32) & 0xFF); | ||||
|     this->write((value >> 40) & 0xFF); | ||||
|     this->write((value >> 48) & 0xFF); | ||||
|     this->write((value >> 56) & 0xFF); | ||||
|   } | ||||
|   template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) { | ||||
|     this->encode_uint32(field_id, static_cast<uint32_t>(value), force); | ||||
|   } | ||||
| @@ -220,12 +236,22 @@ class ProtoWriteBuffer { | ||||
|   } | ||||
|   void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { | ||||
|     uint32_t uvalue; | ||||
|     if (value < 0) | ||||
|     if (value < 0) { | ||||
|       uvalue = ~(value << 1); | ||||
|     else | ||||
|     } else { | ||||
|       uvalue = value << 1; | ||||
|     } | ||||
|     this->encode_uint32(field_id, uvalue, force); | ||||
|   } | ||||
|   void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { | ||||
|     uint64_t uvalue; | ||||
|     if (value < 0) { | ||||
|       uvalue = ~(value << 1); | ||||
|     } else { | ||||
|       uvalue = value << 1; | ||||
|     } | ||||
|     this->encode_uint64(field_id, uvalue, force); | ||||
|   } | ||||
|   template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { | ||||
|     this->encode_field_raw(field_id, 2); | ||||
|     size_t begin = this->buffer_->size(); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_ | ||||
| bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| bool InitialStateIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_state(fan); } | ||||
| bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } | ||||
| @@ -47,8 +47,15 @@ bool InitialStateIterator::on_select(select::Select *select) { | ||||
|   return this->client_->send_select_state(select, select->state); | ||||
| } | ||||
| #endif | ||||
| InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) | ||||
|     : ComponentIterator(server), client_(client) {} | ||||
| #ifdef USE_LOCK | ||||
| bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_state(media_player); | ||||
| } | ||||
| #endif | ||||
| InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| #include "esphome/core/controller.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "util.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| @@ -12,7 +12,7 @@ class APIConnection; | ||||
|  | ||||
| class InitialStateIterator : public ComponentIterator { | ||||
|  public: | ||||
|   InitialStateIterator(APIServer *server, APIConnection *client); | ||||
|   InitialStateIterator(APIConnection *client); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; | ||||
| #endif | ||||
| @@ -20,7 +20,7 @@ class InitialStateIterator : public ComponentIterator { | ||||
|   bool on_cover(cover::Cover *cover) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   bool on_fan(fan::FanState *fan) override; | ||||
|   bool on_fan(fan::Fan *fan) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|   bool on_light(light::LightState *light) override; | ||||
| @@ -45,6 +45,12 @@ class InitialStateIterator : public ComponentIterator { | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   bool on_select(select::Select *select) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool on_lock(lock::Lock *a_lock) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||
| #endif | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
| @@ -52,5 +58,3 @@ class InitialStateIterator : public ComponentIterator { | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|  | ||||
| #include "api_server.h" | ||||
|   | ||||
| @@ -52,7 +52,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { | ||||
|  | ||||
|  protected: | ||||
|   virtual void execute(Ts... x) = 0; | ||||
|   template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) { | ||||
|   template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...> type) { | ||||
|     this->execute((get_execute_arg_value<Ts>(args[S]))...); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -58,10 +58,11 @@ void AS3935Component::loop() { | ||||
|  | ||||
| void AS3935Component::write_indoor(bool indoor) { | ||||
|   ESP_LOGV(TAG, "Setting indoor to %d", indoor); | ||||
|   if (indoor) | ||||
|   if (indoor) { | ||||
|     this->write_register(AFE_GAIN, GAIN_MASK, INDOOR, 1); | ||||
|   else | ||||
|   } else { | ||||
|     this->write_register(AFE_GAIN, GAIN_MASK, OUTDOOR, 1); | ||||
|   } | ||||
| } | ||||
| // REG0x01, bits[3:0], manufacturer default: 0010 (2). | ||||
| // This setting determines the threshold for events that trigger the | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from . import AS3935, CONF_AS3935_ID | ||||
|  | ||||
| DEPENDENCIES = ["as3935"] | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( | ||||
| CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( | ||||
|     { | ||||
|         cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), | ||||
|     } | ||||
|   | ||||
| @@ -4,7 +4,6 @@ from esphome.components import sensor | ||||
| from esphome.const import ( | ||||
|     CONF_DISTANCE, | ||||
|     CONF_LIGHTNING_ENERGY, | ||||
|     STATE_CLASS_NONE, | ||||
|     UNIT_KILOMETER, | ||||
|     ICON_SIGNAL_DISTANCE_VARIANT, | ||||
|     ICON_FLASH, | ||||
| @@ -20,12 +19,10 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             unit_of_measurement=UNIT_KILOMETER, | ||||
|             icon=ICON_SIGNAL_DISTANCE_VARIANT, | ||||
|             accuracy_decimals=1, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|         ), | ||||
|         cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( | ||||
|             icon=ICON_FLASH, | ||||
|             accuracy_decimals=1, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|   | ||||
| @@ -45,6 +45,8 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device | ||||
|       this->battery_voltage_->publish_state(*res->battery_voltage); | ||||
|     success = true; | ||||
|   } | ||||
|   if (this->signal_strength_ != nullptr) | ||||
|     this->signal_strength_->publish_state(device.get_rssi()); | ||||
|  | ||||
|   return success; | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } | ||||
|   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } | ||||
|   void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } | ||||
|   void set_signal_strength(sensor::Sensor *signal_strength) { signal_strength_ = signal_strength; } | ||||
|  | ||||
|  protected: | ||||
|   uint64_t address_; | ||||
| @@ -35,6 +36,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice | ||||
|   sensor::Sensor *humidity_{nullptr}; | ||||
|   sensor::Sensor *battery_level_{nullptr}; | ||||
|   sensor::Sensor *battery_voltage_{nullptr}; | ||||
|   sensor::Sensor *signal_strength_{nullptr}; | ||||
|  | ||||
|   optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data); | ||||
|   bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result); | ||||
|   | ||||
| @@ -6,15 +6,18 @@ from esphome.const import ( | ||||
|     CONF_BATTERY_VOLTAGE, | ||||
|     CONF_MAC_ADDRESS, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_SIGNAL_STRENGTH, | ||||
|     CONF_TEMPERATURE, | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_DECIBEL_MILLIWATT, | ||||
|     UNIT_PERCENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| @@ -59,6 +62,13 @@ CONFIG_SCHEMA = ( | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|             cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_DECIBEL_MILLIWATT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_SIGNAL_STRENGTH, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||
| @@ -85,3 +95,6 @@ async def to_code(config): | ||||
|     if CONF_BATTERY_VOLTAGE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) | ||||
|         cg.add(var.set_battery_voltage(sens)) | ||||
|     if CONF_SIGNAL_STRENGTH in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) | ||||
|         cg.add(var.set_signal_strength(sens)) | ||||
|   | ||||
| @@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   const auto &data = service_data.data; | ||||
|  | ||||
|   const uint8_t protocol_version = data[0] >> 4; | ||||
|   if (protocol_version != 1) { | ||||
|   if (protocol_version != 1 && protocol_version != 2) { | ||||
|     ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); | ||||
|     return false; | ||||
|   } | ||||
| @@ -57,9 +57,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   uint16_t battery_millivolt = data[2] << 8 | data[3]; | ||||
|   float battery_voltage = battery_millivolt / 1000.0f; | ||||
|  | ||||
|   // Temperature in 1000 * Celsius. | ||||
|   uint16_t temp_millicelcius = data[4] << 8 | data[5]; | ||||
|   float temp_celcius = temp_millicelcius / 1000.0f; | ||||
|   // Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2). | ||||
|   float temp_celsius; | ||||
|   if (protocol_version == 1) { | ||||
|     uint16_t temp_millicelsius = data[4] << 8 | data[5]; | ||||
|     temp_celsius = temp_millicelsius / 1000.0f; | ||||
|   } else { | ||||
|     int16_t temp_centicelsius = data[4] << 8 | data[5]; | ||||
|     temp_celsius = temp_centicelsius / 100.0f; | ||||
|   } | ||||
|  | ||||
|   // Relative air humidity in the range [0, 2^16). | ||||
|   uint16_t humidity = data[6] << 8 | data[7]; | ||||
| @@ -76,7 +82,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|     battery_voltage_->publish_state(battery_voltage); | ||||
|   } | ||||
|   if (temperature_ != nullptr) { | ||||
|     temperature_->publish_state(temp_celcius); | ||||
|     temperature_->publish_state(temp_celsius); | ||||
|   } | ||||
|   if (humidity_ != nullptr) { | ||||
|     humidity_->publish_state(humidity_percent); | ||||
|   | ||||
| @@ -97,7 +97,7 @@ void BalluClimate::transmit_state() { | ||||
|  | ||||
|   // Send code | ||||
|   auto transmit = this->transmitter_->transmit(); | ||||
|   auto data = transmit.get_data(); | ||||
|   auto *data = transmit.get_data(); | ||||
|  | ||||
|   data->set_carrier_frequency(38000); | ||||
|  | ||||
| @@ -130,10 +130,10 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) { | ||||
|   for (int i = 0; i < BALLU_STATE_LENGTH; i++) { | ||||
|     // Read bit | ||||
|     for (int j = 0; j < 8; j++) { | ||||
|       if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) | ||||
|       if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) { | ||||
|         remote_state[i] |= 1 << j; | ||||
|  | ||||
|       else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) { | ||||
|       } else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) { | ||||
|         ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); | ||||
|         return false; | ||||
|       } | ||||
|   | ||||
| @@ -21,12 +21,13 @@ void BangBangClimate::setup() { | ||||
|     restore->to_call(this).perform(); | ||||
|   } else { | ||||
|     // restore from defaults, change_away handles those for us | ||||
|     if (supports_cool_ && supports_heat_) | ||||
|     if (supports_cool_ && supports_heat_) { | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||
|     else if (supports_cool_) | ||||
|     } else if (supports_cool_) { | ||||
|       this->mode = climate::CLIMATE_MODE_COOL; | ||||
|     else if (supports_heat_) | ||||
|     } else if (supports_heat_) { | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|     } | ||||
|     this->change_away_(false); | ||||
|   } | ||||
| } | ||||
| @@ -56,11 +57,12 @@ climate::ClimateTraits BangBangClimate::traits() { | ||||
|   if (supports_cool_ && supports_heat_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); | ||||
|   traits.set_supports_two_point_target_temperature(true); | ||||
|   if (supports_away_) | ||||
|   if (supports_away_) { | ||||
|     traits.set_supported_presets({ | ||||
|         climate::CLIMATE_PRESET_HOME, | ||||
|         climate::CLIMATE_PRESET_AWAY, | ||||
|     }); | ||||
|   } | ||||
|   traits.set_supports_action(true); | ||||
|   return traits; | ||||
| } | ||||
| @@ -80,21 +82,25 @@ void BangBangClimate::compute_state_() { | ||||
|  | ||||
|   climate::ClimateAction target_action; | ||||
|   if (too_cold) { | ||||
|     // too cold -> enable heating if possible, else idle | ||||
|     if (this->supports_heat_) | ||||
|     // too cold -> enable heating if possible and enabled, else idle | ||||
|     if (this->supports_heat_ && | ||||
|         (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_HEAT)) { | ||||
|       target_action = climate::CLIMATE_ACTION_HEATING; | ||||
|     else | ||||
|     } else { | ||||
|       target_action = climate::CLIMATE_ACTION_IDLE; | ||||
|     } | ||||
|   } else if (too_hot) { | ||||
|     // too hot -> enable cooling if possible, else idle | ||||
|     if (this->supports_cool_) | ||||
|     // too hot -> enable cooling if possible and enabled, else idle | ||||
|     if (this->supports_cool_ && | ||||
|         (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_COOL)) { | ||||
|       target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|     else | ||||
|     } else { | ||||
|       target_action = climate::CLIMATE_ACTION_IDLE; | ||||
|     } | ||||
|   } else { | ||||
|     // neither too hot nor too cold -> in range | ||||
|     if (this->supports_cool_ && this->supports_heat_) { | ||||
|       // if supports both ends, go to idle action | ||||
|     if (this->supports_cool_ && this->supports_heat_ && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { | ||||
|       // if supports both ends and both cooling and heating enabled, go to idle action | ||||
|       target_action = climate::CLIMATE_ACTION_IDLE; | ||||
|     } else { | ||||
|       // else use current mode and don't change (hysteresis) | ||||
| @@ -105,9 +111,10 @@ void BangBangClimate::compute_state_() { | ||||
|   this->switch_to_action_(target_action); | ||||
| } | ||||
| void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | ||||
|   if (action == this->action) | ||||
|   if (action == this->action) { | ||||
|     // already in target mode | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || | ||||
|       (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@jhansche"] | ||||
							
								
								
									
										675
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										675
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,675 @@ | ||||
| #include "bedjet.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| using namespace esphome::climate; | ||||
|  | ||||
| /// Converts a BedJet temp step into degrees Celsius. | ||||
| float bedjet_temp_to_c(const uint8_t temp) { | ||||
|   // BedJet temp is "C*2"; to get C, divide by 2. | ||||
|   return temp / 2.0f; | ||||
| } | ||||
|  | ||||
| /// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. | ||||
| uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { | ||||
|   //  0 =  5% | ||||
|   // 19 = 100% | ||||
|   return 5 * fan + 5; | ||||
| } | ||||
|  | ||||
| static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { | ||||
|   if (fan_step >= 0 && fan_step <= 19) | ||||
|     return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; | ||||
|   return nullptr; | ||||
| } | ||||
|  | ||||
| static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { | ||||
|   for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { | ||||
|     if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { | ||||
|       return i; | ||||
|     } | ||||
|   } | ||||
|   return -1; | ||||
| } | ||||
|  | ||||
| static BedjetButton heat_button(BedjetHeatMode mode) { | ||||
|   BedjetButton btn = BTN_HEAT; | ||||
|   if (mode == HEAT_MODE_EXTENDED) { | ||||
|     btn = BTN_EXTHT; | ||||
|   } | ||||
|   return btn; | ||||
| } | ||||
|  | ||||
| void Bedjet::upgrade_firmware() { | ||||
|   auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::dump_config() { | ||||
|   LOG_CLIMATE("", "BedJet Climate", this); | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported modes:"); | ||||
|   for (auto mode : traits.get_supported_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported fan modes:"); | ||||
|   for (const auto &mode : traits.get_supported_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); | ||||
|   } | ||||
|   for (const auto &mode : traits.get_supported_custom_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str()); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported presets:"); | ||||
|   for (auto preset : traits.get_supported_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|   } | ||||
|   for (const auto &preset : traits.get_supported_custom_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::setup() { | ||||
|   this->codec_ = make_unique<BedjetCodec>(); | ||||
|  | ||||
|   // restore set points | ||||
|   auto restore = this->restore_state_(); | ||||
|   if (restore.has_value()) { | ||||
|     ESP_LOGI(TAG, "Restored previous saved state."); | ||||
|     restore->apply(this); | ||||
|   } else { | ||||
|     // Initial status is unknown until we connect | ||||
|     this->reset_state_(); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   this->setup_time_(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| /** Resets states to defaults. */ | ||||
| void Bedjet::reset_state_() { | ||||
|   this->mode = climate::CLIMATE_MODE_OFF; | ||||
|   this->action = climate::CLIMATE_ACTION_IDLE; | ||||
|   this->target_temperature = NAN; | ||||
|   this->current_temperature = NAN; | ||||
|   this->preset.reset(); | ||||
|   this->custom_preset.reset(); | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void Bedjet::loop() {} | ||||
|  | ||||
| void Bedjet::control(const ClimateCall &call) { | ||||
|   ESP_LOGD(TAG, "Received Bedjet::control"); | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (call.get_mode().has_value()) { | ||||
|     ClimateMode mode = *call.get_mode(); | ||||
|     BedjetPacket *pkt; | ||||
|     switch (mode) { | ||||
|       case climate::CLIMATE_MODE_OFF: | ||||
|         pkt = this->codec_->get_button_request(BTN_OFF); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_HEAT: | ||||
|         pkt = this->codec_->get_button_request(heat_button(this->heating_mode_)); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_FAN_ONLY: | ||||
|         pkt = this->codec_->get_button_request(BTN_COOL); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_DRY: | ||||
|         pkt = this->codec_->get_button_request(BTN_DRY); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGW(TAG, "Unsupported mode: %d", mode); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|       this->mode = mode; | ||||
|       // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     auto target_temp = *call.get_target_temperature(); | ||||
|     auto *pkt = this->codec_->get_set_target_temp_request(target_temp); | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->target_temperature = target_temp; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_preset().has_value()) { | ||||
|     ClimatePreset preset = *call.get_preset(); | ||||
|     BedjetPacket *pkt; | ||||
|  | ||||
|     if (preset == climate::CLIMATE_PRESET_BOOST) { | ||||
|       pkt = this->codec_->get_button_request(BTN_TURBO); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %d", preset); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->preset = preset; | ||||
|       this->custom_preset.reset(); | ||||
|       this->force_refresh_ = true; | ||||
|     } | ||||
|   } else if (call.get_custom_preset().has_value()) { | ||||
|     std::string preset = *call.get_custom_preset(); | ||||
|     BedjetPacket *pkt; | ||||
|  | ||||
|     if (preset == "M1") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M1); | ||||
|     } else if (preset == "M2") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M2); | ||||
|     } else if (preset == "M3") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M3); | ||||
|     } else if (preset == "LTD HT") { | ||||
|       pkt = this->codec_->get_button_request(BTN_HEAT); | ||||
|     } else if (preset == "EXT HT") { | ||||
|       pkt = this->codec_->get_button_request(BTN_EXTHT); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|       this->custom_preset = preset; | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_fan_mode().has_value()) { | ||||
|     // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. | ||||
|     // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. | ||||
|     auto fan_mode = *call.get_fan_mode(); | ||||
|     BedjetPacket *pkt; | ||||
|     if (fan_mode == climate::CLIMATE_FAN_LOW) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); | ||||
|     } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); | ||||
|     } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), | ||||
|                LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|     } | ||||
|   } else if (call.get_custom_fan_mode().has_value()) { | ||||
|     auto fan_mode = *call.get_custom_fan_mode(); | ||||
|     auto fan_step = bedjet_fan_speed_to_step(fan_mode); | ||||
|     if (fan_step >= 0 && fan_step <= 19) { | ||||
|       ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), | ||||
|                fan_step); | ||||
|       // The index should represent the fan_step index. | ||||
|       BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); | ||||
|       auto status = this->write_bedjet_packet_(pkt); | ||||
|       if (status) { | ||||
|         ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|       } else { | ||||
|         this->force_refresh_ = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); | ||||
|       this->status_set_warning(); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->char_handle_cmd_ = chr->handle; | ||||
|  | ||||
|       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->char_handle_status_ = chr->handle; | ||||
|       // We also need to obtain the config descriptor for this handle. | ||||
|       // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be | ||||
|       // able to look it up. | ||||
|       auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); | ||||
|       if (descr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", | ||||
|                  this->char_handle_status_); | ||||
|       } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || | ||||
|                  descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { | ||||
|         ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, | ||||
|                  descr->uuid.to_string().c_str()); | ||||
|       } else { | ||||
|         this->config_descr_status_ = descr->handle; | ||||
|       } | ||||
|  | ||||
|       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); | ||||
|       if (chr != nullptr) { | ||||
|         this->char_handle_name_ = chr->handle; | ||||
|         auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, | ||||
|                                               ESP_GATT_AUTH_REQ_NONE); | ||||
|         if (status) { | ||||
|           ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ESP_LOGD(TAG, "Services complete: obtained char handles."); | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|  | ||||
|       this->set_notify_(true); | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|       if (this->time_id_.has_value()) { | ||||
|         this->send_local_time(); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_DESCR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         // ESP_GATT_INVALID_ATTR_LEN | ||||
|         ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||
|         break; | ||||
|       } | ||||
|       // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 | ||||
|       // This might be the enable-notify descriptor? (or disable-notify) | ||||
|       ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, | ||||
|                param->write.status); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_CHAR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->write.handle == this->char_handle_cmd_) { | ||||
|         if (this->force_refresh_) { | ||||
|           // Command write was successful. Publish the pending state, hoping that notify will kick in. | ||||
|           this->publish_state(); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent_->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->char_handle_status_) { | ||||
|         // This is the additional packet that doesn't fit in the notify packet. | ||||
|         this->codec_->decode_extra(param->read.value, param->read.value_len); | ||||
|       } else if (param->read.handle == this->char_handle_name_) { | ||||
|         // The data should represent the name. | ||||
|         if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { | ||||
|           std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len); | ||||
|           // this->set_name(bedjet_name); | ||||
|           ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       // This event means that ESP received the request to enable notifications on the client side. But we also have to | ||||
|       // tell the server that we want it to send notifications. Normally BLEClient parent would handle this | ||||
|       // automatically, but as soon as we set our status to Established, the parent is going to purge all the | ||||
|       // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable | ||||
|       // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write | ||||
|       // doesn't break anything. | ||||
|  | ||||
|       if (param->reg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(true); | ||||
|       this->last_notify_ = 0; | ||||
|       this->force_refresh_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||
|       // This event is not handled by the parent BLEClient, so we need to do this either way. | ||||
|       if (param->unreg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(false); | ||||
|       this->last_notify_ = 0; | ||||
|       // Now we wait until the next update() poll to re-register notify... | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (param->notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), | ||||
|                  this->char_handle_status_, param->notify.handle); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we | ||||
|       //  throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). | ||||
|       //  Another idea would be to keep notify off by default, and use update() as an opportunity to turn on | ||||
|       //  notify to get enough data to update status, then turn off notify again. | ||||
|  | ||||
|       uint32_t now = millis(); | ||||
|       auto delta = now - this->last_notify_; | ||||
|  | ||||
|       if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { | ||||
|         bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); | ||||
|         this->last_notify_ = now; | ||||
|  | ||||
|         if (needs_extra) { | ||||
|           // this means the packet was partial, so read the status characteristic to get the second part. | ||||
|           auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, | ||||
|                                                 this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) { | ||||
|             ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (this->force_refresh_) { | ||||
|           // If we requested an immediate update, do that now. | ||||
|           this->update(); | ||||
|           this->force_refresh_ = false; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. | ||||
|  * | ||||
|  * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order | ||||
|  * to undo the same on unregister. It also allows us to maintain the config descriptor separately, | ||||
|  * since the parent BLEClient is going to purge all descriptors once we set our connection status | ||||
|  * to `Established`. | ||||
|  */ | ||||
| uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { | ||||
|   auto handle = this->config_descr_status_; | ||||
|   if (handle == 0) { | ||||
|     ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); | ||||
|     return -1; | ||||
|   } | ||||
|  | ||||
|   // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. | ||||
|   uint8_t notify_en[] = {0, 0}; | ||||
|   notify_en[0] = enable; | ||||
|   auto status = | ||||
|       esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), | ||||
|                                      ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); | ||||
|     return status; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", | ||||
|            handle); | ||||
|   return ESP_GATT_OK; | ||||
| } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
| /** Attempts to sync the local time (via `time_id`) to the BedJet device. */ | ||||
| void Bedjet::send_local_time() { | ||||
|   if (this->time_id_.has_value()) { | ||||
|     auto *time_id = *this->time_id_; | ||||
|     time::ESPTime now = time_id->now(); | ||||
|     if (now.is_valid()) { | ||||
|       this->set_clock(now.hour, now.minute); | ||||
|       ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Initializes time sync callbacks to support syncing current time to the BedJet. */ | ||||
| void Bedjet::setup_time_() { | ||||
|   if (this->time_id_.has_value()) { | ||||
|     this->send_local_time(); | ||||
|     auto *time_id = *this->time_id_; | ||||
|     time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| /** Attempt to set the BedJet device's clock to the specified time. */ | ||||
| void Bedjet::set_clock(uint8_t hour, uint8_t minute) { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ | ||||
| uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     if (!this->parent_->enabled) { | ||||
|       ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); | ||||
|     } | ||||
|     return -1; | ||||
|   } | ||||
|   auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, | ||||
|                                          pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                          ESP_GATT_AUTH_REQ_NONE); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| /** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ | ||||
| uint8_t Bedjet::set_notify_(const bool enable) { | ||||
|   uint8_t status; | ||||
|   if (enable) { | ||||
|     status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } else { | ||||
|     status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                  this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| /** Attempts to update the climate device from the last received BedjetStatusPacket. | ||||
|  * | ||||
|  * @return `true` if the status has been applied; `false` if there is nothing to apply. | ||||
|  */ | ||||
| bool Bedjet::update_status_() { | ||||
|   if (!this->codec_->has_status()) | ||||
|     return false; | ||||
|  | ||||
|   BedjetStatusPacket status = *this->codec_->get_status_packet(); | ||||
|  | ||||
|   auto converted_temp = bedjet_temp_to_c(status.target_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->target_temperature = converted_temp; | ||||
|   converted_temp = bedjet_temp_to_c(status.ambient_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->current_temperature = converted_temp; | ||||
|  | ||||
|   const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); | ||||
|   if (fan_mode_name != nullptr) { | ||||
|     this->custom_fan_mode = *fan_mode_name; | ||||
|   } | ||||
|  | ||||
|   // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. | ||||
|   switch (status.mode) { | ||||
|     case MODE_WAIT:  // Biorhythm "wait" step: device is idle | ||||
|     case MODE_STANDBY: | ||||
|       this->mode = climate::CLIMATE_MODE_OFF; | ||||
|       this->action = climate::CLIMATE_ACTION_IDLE; | ||||
|       this->fan_mode = climate::CLIMATE_FAN_OFF; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_HEAT: | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->action = climate::CLIMATE_ACTION_HEATING; | ||||
|       this->preset.reset(); | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->set_custom_preset_("LTD HT"); | ||||
|       } else { | ||||
|         this->custom_preset.reset(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case MODE_EXTHT: | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->action = climate::CLIMATE_ACTION_HEATING; | ||||
|       this->preset.reset(); | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->custom_preset.reset(); | ||||
|       } else { | ||||
|         this->set_custom_preset_("EXT HT"); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case MODE_COOL: | ||||
|       this->mode = climate::CLIMATE_MODE_FAN_ONLY; | ||||
|       this->action = climate::CLIMATE_ACTION_COOLING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_DRY: | ||||
|       this->mode = climate::CLIMATE_MODE_DRY; | ||||
|       this->action = climate::CLIMATE_ACTION_DRYING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_TURBO: | ||||
|       this->preset = climate::CLIMATE_PRESET_BOOST; | ||||
|       this->custom_preset.reset(); | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->action = climate::CLIMATE_ACTION_HEATING; | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   if (this->is_valid_()) { | ||||
|     this->publish_state(); | ||||
|     this->codec_->clear_status(); | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void Bedjet::update() { | ||||
|   ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     if (!this->parent()->enabled) { | ||||
|       ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); | ||||
|     } else { | ||||
|       // Possibly still trying to connect. | ||||
|       ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); | ||||
|     } | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto result = this->update_status_(); | ||||
|   if (!result) { | ||||
|     uint32_t now = millis(); | ||||
|     uint32_t diff = now - this->last_notify_; | ||||
|  | ||||
|     if (this->last_notify_ == 0) { | ||||
|       // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. | ||||
|       // However, it could also mean that it's running, but failing to send notifications. | ||||
|       // We can try to unregister for notifications now, and then re-register, hoping to clear it up... | ||||
|       // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? | ||||
|  | ||||
|       ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); | ||||
|       this->set_notify_(false); | ||||
|     } else if (diff > NOTIFY_WARN_THRESHOLD) { | ||||
|       ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); | ||||
|     } | ||||
|  | ||||
|     if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); | ||||
|       this->parent()->set_enabled(false); | ||||
|       this->parent()->set_enabled(true); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										133
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "bedjet_base.h" | ||||
|  | ||||
| #ifdef USE_TIME | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); | ||||
|  | ||||
| class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } | ||||
|   void send_local_time(); | ||||
| #endif | ||||
|   void set_clock(uint8_t hour, uint8_t minute); | ||||
|   void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||
|   /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ | ||||
|   void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } | ||||
|  | ||||
|   /** Attempts to check for and apply firmware updates. */ | ||||
|   void upgrade_firmware(); | ||||
|  | ||||
|   climate::ClimateTraits traits() override { | ||||
|     auto traits = climate::ClimateTraits(); | ||||
|     traits.set_supports_action(true); | ||||
|     traits.set_supports_current_temperature(true); | ||||
|     traits.set_supported_modes({ | ||||
|         climate::CLIMATE_MODE_OFF, | ||||
|         climate::CLIMATE_MODE_HEAT, | ||||
|         // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead | ||||
|         climate::CLIMATE_MODE_FAN_ONLY, | ||||
|         climate::CLIMATE_MODE_DRY, | ||||
|     }); | ||||
|  | ||||
|     // It would be better if we had a slider for the fan modes. | ||||
|     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); | ||||
|     traits.set_supported_presets({ | ||||
|         // If we support NONE, then have to decide what happens if the user switches to it (turn off?) | ||||
|         // climate::CLIMATE_PRESET_NONE, | ||||
|         // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. | ||||
|         climate::CLIMATE_PRESET_BOOST, | ||||
|     }); | ||||
|     traits.set_supported_custom_presets({ | ||||
|         // We could fetch biodata from bedjet and set these names that way. | ||||
|         // But then we have to invert the lookup in order to send the right preset. | ||||
|         // For now, we can leave them as M1-3 to match the remote buttons. | ||||
|         // EXT HT added to match remote button. | ||||
|         "EXT HT", | ||||
|         "M1", | ||||
|         "M2", | ||||
|         "M3", | ||||
|     }); | ||||
|     if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|       traits.add_supported_custom_preset("LTD HT"); | ||||
|     } else { | ||||
|       traits.add_supported_custom_preset("EXT HT"); | ||||
|     } | ||||
|     traits.set_visual_min_temperature(19.0); | ||||
|     traits.set_visual_max_temperature(43.0); | ||||
|     traits.set_visual_temperature_step(1.0); | ||||
|     return traits; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void control(const climate::ClimateCall &call) override; | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   void setup_time_(); | ||||
|   optional<time::RealTimeClock *> time_id_{}; | ||||
| #endif | ||||
|  | ||||
|   uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; | ||||
|   BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; | ||||
|  | ||||
|   static const uint32_t MIN_NOTIFY_THROTTLE = 5000; | ||||
|   static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; | ||||
|   static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; | ||||
|  | ||||
|   uint8_t set_notify_(bool enable); | ||||
|   uint8_t write_bedjet_packet_(BedjetPacket *pkt); | ||||
|   void reset_state_(); | ||||
|   bool update_status_(); | ||||
|  | ||||
|   bool is_valid_() { | ||||
|     // FIXME: find a better way to check this? | ||||
|     return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && | ||||
|            this->current_temperature > 1 && this->target_temperature > 1; | ||||
|   } | ||||
|  | ||||
|   uint32_t last_notify_ = 0; | ||||
|   bool force_refresh_ = false; | ||||
|  | ||||
|   std::unique_ptr<BedjetCodec> codec_; | ||||
|   uint16_t char_handle_cmd_; | ||||
|   uint16_t char_handle_name_; | ||||
|   uint16_t char_handle_status_; | ||||
|   uint16_t config_descr_status_; | ||||
|  | ||||
|   uint8_t write_notify_config_descriptor_(bool enable); | ||||
| }; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| #include "bedjet_base.h" | ||||
| #include <cstdio> | ||||
| #include <cstring> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| /// Converts a BedJet temp step into degrees Fahrenheit. | ||||
| float bedjet_temp_to_f(const uint8_t temp) { | ||||
|   // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32. | ||||
|   return 0.9f * temp + 32.0f; | ||||
| } | ||||
|  | ||||
| /** Cleans up the packet before sending. */ | ||||
| BedjetPacket *BedjetCodec::clean_packet_() { | ||||
|   // So far no commands require more than 2 bytes of data. | ||||
|   assert(this->packet_.data_length <= 2); | ||||
|   for (int i = this->packet_.data_length; i < 2; i++) { | ||||
|     this->packet_.data[i] = '\0'; | ||||
|   } | ||||
|   ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]); | ||||
|   return &this->packet_; | ||||
| } | ||||
|  | ||||
| /** Returns a BedjetPacket that will initiate a BedjetButton press. */ | ||||
| BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) { | ||||
|   this->packet_.command = CMD_BUTTON; | ||||
|   this->packet_.data_length = 1; | ||||
|   this->packet_.data[0] = button; | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| /** Returns a BedjetPacket that will set the device's target `temperature`. */ | ||||
| BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) { | ||||
|   this->packet_.command = CMD_SET_TEMP; | ||||
|   this->packet_.data_length = 1; | ||||
|   this->packet_.data[0] = temperature * 2; | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| /** Returns a BedjetPacket that will set the device's target fan speed. */ | ||||
| BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { | ||||
|   this->packet_.command = CMD_SET_FAN; | ||||
|   this->packet_.data_length = 1; | ||||
|   this->packet_.data[0] = fan_step; | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| /** Returns a BedjetPacket that will set the device's current time. */ | ||||
| BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { | ||||
|   this->packet_.command = CMD_SET_TIME; | ||||
|   this->packet_.data_length = 2; | ||||
|   this->packet_.data[0] = hour; | ||||
|   this->packet_.data[1] = minute; | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| /** Decodes the extra bytes that were received after being notified with a partial packet. */ | ||||
| void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { | ||||
|   ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||
|   uint8_t offset = this->last_buffer_size_; | ||||
|   if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { | ||||
|     memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); | ||||
|     ESP_LOGV(TAG, | ||||
|              "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " | ||||
|              "flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>", | ||||
|              this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, | ||||
|              this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', | ||||
|              this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', | ||||
|              this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, | ||||
|              sizeof(BedjetStatusPacket), length + offset); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Decodes the incoming status packet received on the BEDJET_STATUS_UUID. | ||||
|  * | ||||
|  * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. | ||||
|  */ | ||||
| bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { | ||||
|   ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||
|  | ||||
|   if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { | ||||
|     this->status_packet_.reset(); | ||||
|  | ||||
|     // Clear old buffer | ||||
|     memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); | ||||
|     // Copy new data into buffer | ||||
|     memcpy(&this->buf_, data, length); | ||||
|     this->last_buffer_size_ = length; | ||||
|  | ||||
|     // TODO: validate the packet checksum? | ||||
|     if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && | ||||
|         this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && | ||||
|         this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { | ||||
|       // and save it for the update() loop | ||||
|       this->status_packet_ = this->buf_; | ||||
|       return this->buf_.is_partial == 1; | ||||
|     } else { | ||||
|       // TODO: log a warning if we detect that we connected to a non-V3 device. | ||||
|       ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); | ||||
|     } | ||||
|   } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { | ||||
|     // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. | ||||
|     ESP_LOGV(TAG, | ||||
|              "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF;  [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " | ||||
|              "[12]=%d, [-1]=%d", | ||||
|              bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], | ||||
|              data[10], data[11], data[12], data[length - 1]); | ||||
|  | ||||
|     if (this->has_status()) { | ||||
|       this->status_packet_->ambient_temp_step = data[6]; | ||||
|     } | ||||
|   } else { | ||||
|     // TODO: log a warning if we detect that we connected to a non-V3 device. | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
							
								
								
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include "bedjet_const.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| struct BedjetPacket { | ||||
|   uint8_t data_length; | ||||
|   BedjetCommand command; | ||||
|   uint8_t data[2]; | ||||
| }; | ||||
|  | ||||
| struct BedjetFlags { | ||||
|   /* uint8_t */ | ||||
|   int a_ : 1;                // 0x80 | ||||
|   int b_ : 1;                // 0x40 | ||||
|   int conn_test_passed : 1;  ///< (0x20) Bit is set `1` if the last connection test passed. | ||||
|   int leds_enabled : 1;      ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. | ||||
|   int c_ : 1;                // 0x08 | ||||
|   int units_setup : 1;       ///< (0x04) Bit is set `1` if the device's units have been configured. | ||||
|   int d_ : 1;                // 0x02 | ||||
|   int beeps_muted : 1;       ///< (0x01) Bit is set `1` if the device's sound output is muted. | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| enum BedjetPacketFormat : uint8_t { | ||||
|   PACKET_FORMAT_DEBUG = 0x05,    //  5 | ||||
|   PACKET_FORMAT_V3_HOME = 0x56,  // 86 | ||||
| }; | ||||
|  | ||||
| enum BedjetPacketType : uint8_t { | ||||
|   PACKET_TYPE_STATUS = 0x1, | ||||
|   PACKET_TYPE_DEBUG = 0x2, | ||||
| }; | ||||
|  | ||||
| /** The format of a BedJet V3 status packet. */ | ||||
| struct BedjetStatusPacket { | ||||
|   // [0] | ||||
|   uint8_t is_partial : 8;  ///< `1` indicates that this is a partial packet, and more data can be read directly from the | ||||
|                            ///< characteristic. | ||||
|   BedjetPacketFormat packet_format : 8;  ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet | ||||
|                                          ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets. | ||||
|   uint8_t | ||||
|       expecting_length : 8;  ///< The expected total length of the status packet after merging the additional packet. | ||||
|   BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. | ||||
|  | ||||
|   // [4] | ||||
|   uint8_t time_remaining_hrs : 8;   ///< Hours remaining in program runtime | ||||
|   uint8_t time_remaining_mins : 8;  ///< Minutes remaining in program runtime | ||||
|   uint8_t time_remaining_secs : 8;  ///< Seconds remaining in program runtime | ||||
|  | ||||
|   // [7] | ||||
|   uint8_t actual_temp_step : 8;  ///< Actual temp of the air blown by the BedJet fan; value represents `2 * | ||||
|                                  ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f | ||||
|   uint8_t target_temp_step : 8;  ///< Target temp that the BedJet will try to heat to. See #actual_temp_step. | ||||
|  | ||||
|   // [9] | ||||
|   BedjetMode mode : 8;  ///< BedJet operating mode. | ||||
|  | ||||
|   // [10] | ||||
|   uint8_t fan_step : 8;  ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5 | ||||
|                          ///< * fan_step` | ||||
|   uint8_t max_hrs : 8;   ///< Max hours of mode runtime | ||||
|   uint8_t max_mins : 8;  ///< Max minutes of mode runtime | ||||
|   uint8_t min_temp_step : 8;  ///< Min temp allowed in mode. See #actual_temp_step. | ||||
|   uint8_t max_temp_step : 8;  ///< Max temp allowed in mode. See #actual_temp_step. | ||||
|  | ||||
|   // [15-16] | ||||
|   uint16_t turbo_time : 16;  ///< Time remaining in BedjetMode::MODE_TURBO. | ||||
|  | ||||
|   // [17] | ||||
|   uint8_t ambient_temp_step : 8;  ///< Current ambient air temp. This is the coldest air the BedJet can blow. See | ||||
|                                   ///< #actual_temp_step. | ||||
|   uint8_t shutdown_reason : 8;    ///< The reason for the last device shutdown. | ||||
|  | ||||
|   // [19-25]; the initial partial packet cuts off here after [19] | ||||
|   // Skip 7 bytes? | ||||
|   uint32_t _skip_1_ : 32;  // Unknown 19-22 = 0x01810112 | ||||
|  | ||||
|   uint16_t _skip_2_ : 16;  // Unknown 23-24 = 0x1310 | ||||
|   uint8_t _skip_3_ : 8;    // Unknown 25 = 0x00 | ||||
|  | ||||
|   // [26] | ||||
|   //   0x18(24) = "Connection test has completed OK" | ||||
|   //   0x1a(26) = "Firmware update is not needed" | ||||
|   uint8_t update_phase : 8;  ///< The current status/phase of a firmware update. | ||||
|  | ||||
|   // [27] | ||||
|   // FIXME: cannot nest packed struct of matching length here? | ||||
|   /* BedjetFlags */ uint8_t flags : 8;  /// See BedjetFlags for the packed byte flags. | ||||
|   // [28-31]; 20+11 bytes | ||||
|   uint32_t _skip_4_ : 32;  // Unknown | ||||
|  | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| /** This class is responsible for encoding command packets and decoding status packets. | ||||
|  * | ||||
|  * Status Packets | ||||
|  * ============== | ||||
|  * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID | ||||
|  * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off, | ||||
|  * it generally will not notify of any status. | ||||
|  * | ||||
|  * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets, | ||||
|  * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional | ||||
|  * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the | ||||
|  * full status packet. | ||||
|  * | ||||
|  * Command Packets | ||||
|  * =============== | ||||
|  * This class supports encoding a number of BedjetPacket commands: | ||||
|  * - Button press | ||||
|  *   This simulates a press of one of the BedjetButton values. | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_BUTTON | ||||
|  *   - BedjetPacket#data [0] contains the BedjetButton value | ||||
|  * - Set target temp | ||||
|  *   This sets the BedJet's target temp to a concrete temperature value. | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP | ||||
|  *   - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step | ||||
|  * - Set fan speed | ||||
|  *   This sets the BedJet fan speed. | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_SET_FAN | ||||
|  *   - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19. | ||||
|  * - Set current time | ||||
|  *   The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might | ||||
|  *   contain time-of-day based step rules. | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TIME | ||||
|  *   - BedjetPacket#data [0] is hours, [1] is minutes | ||||
|  */ | ||||
| class BedjetCodec { | ||||
|  public: | ||||
|   BedjetPacket *get_button_request(BedjetButton button); | ||||
|   BedjetPacket *get_set_target_temp_request(float temperature); | ||||
|   BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); | ||||
|   BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); | ||||
|  | ||||
|   bool decode_notify(const uint8_t *data, uint16_t length); | ||||
|   void decode_extra(const uint8_t *data, uint16_t length); | ||||
|  | ||||
|   inline bool has_status() { return this->status_packet_.has_value(); } | ||||
|   const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; } | ||||
|   void clear_status() { this->status_packet_.reset(); } | ||||
|  | ||||
|  protected: | ||||
|   BedjetPacket *clean_packet_(); | ||||
|  | ||||
|   uint8_t last_buffer_size_ = 0; | ||||
|  | ||||
|   BedjetPacket packet_; | ||||
|  | ||||
|   optional<BedjetStatusPacket> status_packet_; | ||||
|   BedjetStatusPacket buf_; | ||||
| }; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
							
								
								
									
										86
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| static const char *const TAG = "bedjet"; | ||||
|  | ||||
| enum BedjetMode : uint8_t { | ||||
|   /// BedJet is Off | ||||
|   MODE_STANDBY = 0, | ||||
|   /// BedJet is in Heat mode (limited to 4 hours) | ||||
|   MODE_HEAT = 1, | ||||
|   /// BedJet is in Turbo mode (high heat, limited time) | ||||
|   MODE_TURBO = 2, | ||||
|   /// BedJet is in Extended Heat mode (limited to 10 hours) | ||||
|   MODE_EXTHT = 3, | ||||
|   /// BedJet is in Cool mode (actually "Fan only" mode) | ||||
|   MODE_COOL = 4, | ||||
|   /// BedJet is in Dry mode (high speed, no heat) | ||||
|   MODE_DRY = 5, | ||||
|   /// BedJet is in "wait" mode, a step during a biorhythm program | ||||
|   MODE_WAIT = 6, | ||||
| }; | ||||
|  | ||||
| /** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */ | ||||
| enum BedjetHeatMode { | ||||
|   /// HVACMode.HEAT is handled using BTN_HEAT (default) | ||||
|   HEAT_MODE_HEAT, | ||||
|   /// HVACMode.HEAT is handled using BTN_EXTHT | ||||
|   HEAT_MODE_EXTENDED, | ||||
| }; | ||||
|  | ||||
| enum BedjetButton : uint8_t { | ||||
|   /// Turn BedJet off | ||||
|   BTN_OFF = 0x1, | ||||
|   /// Enter Cool mode (fan only) | ||||
|   BTN_COOL = 0x2, | ||||
|   /// Enter Heat mode (limited to 4 hours) | ||||
|   BTN_HEAT = 0x3, | ||||
|   /// Enter Turbo mode (high heat, limited to 10 minutes) | ||||
|   BTN_TURBO = 0x4, | ||||
|   /// Enter Dry mode (high speed, no heat) | ||||
|   BTN_DRY = 0x5, | ||||
|   /// Enter Extended Heat mode (limited to 10 hours) | ||||
|   BTN_EXTHT = 0x6, | ||||
|  | ||||
|   /// Start the M1 biorhythm/preset program | ||||
|   BTN_M1 = 0x20, | ||||
|   /// Start the M2 biorhythm/preset program | ||||
|   BTN_M2 = 0x21, | ||||
|   /// Start the M3 biorhythm/preset program | ||||
|   BTN_M3 = 0x22, | ||||
|  | ||||
|   /* These are "MAGIC" buttons */ | ||||
|  | ||||
|   /// Turn debug mode on/off | ||||
|   MAGIC_DEBUG_ON = 0x40, | ||||
|   MAGIC_DEBUG_OFF = 0x41, | ||||
|   /// Perform a connection test. | ||||
|   MAGIC_CONNTEST = 0x42, | ||||
|   /// Request a firmware update. This will also restart the Bedjet. | ||||
|   MAGIC_UPDATE = 0x43, | ||||
| }; | ||||
|  | ||||
| enum BedjetCommand : uint8_t { | ||||
|   CMD_BUTTON = 0x1, | ||||
|   CMD_SET_TEMP = 0x3, | ||||
|   CMD_STATUS = 0x6, | ||||
|   CMD_SET_FAN = 0x7, | ||||
|   CMD_SET_TIME = 0x8, | ||||
| }; | ||||
|  | ||||
| #define BEDJET_FAN_STEP_NAMES_ \ | ||||
|   { \ | ||||
|     "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \ | ||||
|         "85%", "90%", "95%", "100%" \ | ||||
|   } | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
							
								
								
									
										52
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import climate, ble_client, time | ||||
| from esphome.const import ( | ||||
|     CONF_HEAT_MODE, | ||||
|     CONF_ID, | ||||
|     CONF_RECEIVE_TIMEOUT, | ||||
|     CONF_TIME_ID, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@jhansche"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||
| Bedjet = bedjet_ns.class_( | ||||
|     "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||
| ) | ||||
| BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") | ||||
| BEDJET_HEAT_MODES = { | ||||
|     "heat": BedjetHeatMode.HEAT_MODE_HEAT, | ||||
|     "extended": BedjetHeatMode.HEAT_MODE_EXTENDED, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     climate.CLIMATE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Bedjet), | ||||
|             cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( | ||||
|                 BEDJET_HEAT_MODES, lower=True | ||||
|             ), | ||||
|             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||
|             cv.Optional( | ||||
|                 CONF_RECEIVE_TIMEOUT, default="0s" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("30s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await climate.register_climate(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|     cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) | ||||
|     if CONF_TIME_ID in config: | ||||
|         time_ = await cg.get_variable(config[CONF_TIME_ID]) | ||||
|         cg.add(var.set_time_id(time_)) | ||||
|     if CONF_RECEIVE_TIMEOUT in config: | ||||
|         cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) | ||||
| @@ -9,18 +9,109 @@ static const char *const TAG = "bh1750.sensor"; | ||||
| static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; | ||||
| static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000;  // last 3 bits | ||||
| static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000;  // last 5 bits | ||||
| static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011; | ||||
| static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000; | ||||
| static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001; | ||||
|  | ||||
| /* | ||||
| bh1750 properties: | ||||
|  | ||||
| L-resolution mode: | ||||
| - resolution 4lx (@ mtreg=69) | ||||
| - measurement time: typ=16ms, max=24ms, scaled by MTreg value divided by 69 | ||||
| - formula: counts / 1.2 * (69 / MTreg) lx | ||||
| H-resolution mode: | ||||
| - resolution 1lx (@ mtreg=69) | ||||
| - measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69 | ||||
| - formula: counts / 1.2 * (69 / MTreg) lx | ||||
| H-resolution mode2: | ||||
| - resolution 0.5lx (@ mtreg=69) | ||||
| - measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69 | ||||
| - formula: counts / 1.2 * (69 / MTreg) / 2 lx | ||||
|  | ||||
| MTreg: | ||||
| - min=31, default=69, max=254 | ||||
|  | ||||
| -> only reason to use l-resolution is faster, but offers no higher range | ||||
| -> below ~7000lx, makes sense to use H-resolution2 @ MTreg=254 | ||||
| -> try to maximize MTreg to get lowest noise level | ||||
| */ | ||||
|  | ||||
| void BH1750Sensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); | ||||
|   if (!this->write_bytes(BH1750_COMMAND_POWER_ON, nullptr, 0)) { | ||||
|   uint8_t turn_on = BH1750_COMMAND_POWER_ON; | ||||
|   if (this->write(&turn_on, 1) != i2c::ERROR_OK) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   uint8_t mtreg_hi = (this->measurement_duration_ >> 5) & 0b111; | ||||
|   uint8_t mtreg_lo = (this->measurement_duration_ >> 0) & 0b11111; | ||||
|   this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); | ||||
|   this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); | ||||
| void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) { | ||||
|   // turn on (after one-shot sensor automatically powers down) | ||||
|   uint8_t turn_on = BH1750_COMMAND_POWER_ON; | ||||
|   if (this->write(&turn_on, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Turning on BH1750 failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (active_mtreg_ != mtreg) { | ||||
|     // set mtreg | ||||
|     uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); | ||||
|     uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); | ||||
|     if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { | ||||
|       ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); | ||||
|       active_mtreg_ = 0; | ||||
|       f(NAN); | ||||
|       return; | ||||
|     } | ||||
|     active_mtreg_ = mtreg; | ||||
|   } | ||||
|  | ||||
|   uint8_t cmd; | ||||
|   uint16_t meas_time; | ||||
|   switch (mode) { | ||||
|     case BH1750_MODE_L: | ||||
|       cmd = BH1750_COMMAND_ONE_TIME_L; | ||||
|       meas_time = 24 * mtreg / 69; | ||||
|       break; | ||||
|     case BH1750_MODE_H: | ||||
|       cmd = BH1750_COMMAND_ONE_TIME_H; | ||||
|       meas_time = 180 * mtreg / 69; | ||||
|       break; | ||||
|     case BH1750_MODE_H2: | ||||
|       cmd = BH1750_COMMAND_ONE_TIME_H2; | ||||
|       meas_time = 180 * mtreg / 69; | ||||
|       break; | ||||
|     default: | ||||
|       f(NAN); | ||||
|       return; | ||||
|   } | ||||
|   if (this->write(&cmd, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); | ||||
|     f(NAN); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // probably not needed, but adjust for rounding | ||||
|   meas_time++; | ||||
|  | ||||
|   this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { | ||||
|     uint16_t raw_value; | ||||
|     if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { | ||||
|       ESP_LOGW(TAG, "Reading BH1750 data failed"); | ||||
|       f(NAN); | ||||
|       return; | ||||
|     } | ||||
|     raw_value = i2c::i2ctohs(raw_value); | ||||
|  | ||||
|     float lx = float(raw_value) / 1.2f; | ||||
|     lx *= 69.0f / mtreg; | ||||
|     if (mode == BH1750_MODE_H2) | ||||
|       lx /= 2.0f; | ||||
|  | ||||
|     f(lx); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void BH1750Sensor::dump_config() { | ||||
| @@ -30,64 +121,49 @@ void BH1750Sensor::dump_config() { | ||||
|     ESP_LOGE(TAG, "Communication with BH1750 failed!"); | ||||
|   } | ||||
|  | ||||
|   const char *resolution_s; | ||||
|   switch (this->resolution_) { | ||||
|     case BH1750_RESOLUTION_0P5_LX: | ||||
|       resolution_s = "0.5"; | ||||
|       break; | ||||
|     case BH1750_RESOLUTION_1P0_LX: | ||||
|       resolution_s = "1"; | ||||
|       break; | ||||
|     case BH1750_RESOLUTION_4P0_LX: | ||||
|       resolution_s = "4"; | ||||
|       break; | ||||
|     default: | ||||
|       resolution_s = "Unknown"; | ||||
|       break; | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Resolution: %s", resolution_s); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| void BH1750Sensor::update() { | ||||
|   if (!this->write_bytes(this->resolution_, nullptr, 0)) | ||||
|     return; | ||||
|   // first do a quick measurement in L-mode with full range | ||||
|   // to find right range | ||||
|   this->read_lx_(BH1750_MODE_L, 31, [this](float val) { | ||||
|     if (std::isnan(val)) { | ||||
|       this->status_set_warning(); | ||||
|       this->publish_state(NAN); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   uint32_t wait = 0; | ||||
|   // use max conversion times | ||||
|   switch (this->resolution_) { | ||||
|     case BH1750_RESOLUTION_0P5_LX: | ||||
|     case BH1750_RESOLUTION_1P0_LX: | ||||
|       wait = 180; | ||||
|       break; | ||||
|     case BH1750_RESOLUTION_4P0_LX: | ||||
|       wait = 24; | ||||
|       break; | ||||
|   } | ||||
|     BH1750Mode use_mode; | ||||
|     uint8_t use_mtreg; | ||||
|     if (val <= 7000) { | ||||
|       use_mode = BH1750_MODE_H2; | ||||
|       use_mtreg = 254; | ||||
|     } else { | ||||
|       use_mode = BH1750_MODE_H; | ||||
|       // lx = counts / 1.2 * (69 / mtreg) | ||||
|       // -> mtreg = counts / 1.2 * (69 / lx) | ||||
|       // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg) | ||||
|       // -> mtreg = 50000*(10/12)*(69/lx) | ||||
|       int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val); | ||||
|       use_mtreg = std::min(254, std::max(31, ideal_mtreg)); | ||||
|     } | ||||
|     ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg); | ||||
|  | ||||
|   this->set_timeout("illuminance", wait, [this]() { this->read_data_(); }); | ||||
|     this->read_lx_(use_mode, use_mtreg, [this](float val) { | ||||
|       if (std::isnan(val)) { | ||||
|         this->status_set_warning(); | ||||
|         this->publish_state(NAN); | ||||
|         return; | ||||
|       } | ||||
|       ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); | ||||
|       this->status_clear_warning(); | ||||
|       this->publish_state(val); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
| void BH1750Sensor::read_data_() { | ||||
|   uint16_t raw_value; | ||||
|   if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   raw_value = i2c::i2ctohs(raw_value); | ||||
|  | ||||
|   float lx = float(raw_value) / 1.2f; | ||||
|   lx *= 69.0f / this->measurement_duration_; | ||||
|   if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) { | ||||
|     lx /= 2.0f; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); | ||||
|   this->publish_state(lx); | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; } | ||||
|  | ||||
| }  // namespace bh1750 | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -7,29 +7,15 @@ | ||||
| namespace esphome { | ||||
| namespace bh1750 { | ||||
|  | ||||
| /// Enum listing all resolutions that can be used with the BH1750 | ||||
| enum BH1750Resolution { | ||||
|   BH1750_RESOLUTION_4P0_LX = 0b00100011,  // one-time low resolution mode | ||||
|   BH1750_RESOLUTION_1P0_LX = 0b00100000,  // one-time high resolution mode 1 | ||||
|   BH1750_RESOLUTION_0P5_LX = 0b00100001,  // one-time high resolution mode 2 | ||||
| enum BH1750Mode { | ||||
|   BH1750_MODE_L, | ||||
|   BH1750_MODE_H, | ||||
|   BH1750_MODE_H2, | ||||
| }; | ||||
|  | ||||
| /// This class implements support for the i2c-based BH1750 ambient light sensor. | ||||
| class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   /** Set the resolution of this sensor. | ||||
|    * | ||||
|    * Possible values are: | ||||
|    * | ||||
|    *  - `BH1750_RESOLUTION_4P0_LX` | ||||
|    *  - `BH1750_RESOLUTION_1P0_LX` | ||||
|    *  - `BH1750_RESOLUTION_0P5_LX` (default) | ||||
|    * | ||||
|    * @param resolution The new resolution of the sensor. | ||||
|    */ | ||||
|   void set_resolution(BH1750Resolution resolution); | ||||
|   void set_measurement_duration(uint8_t measurement_duration) { measurement_duration_ = measurement_duration; } | ||||
|  | ||||
|   // ========== INTERNAL METHODS ========== | ||||
|   // (In most use cases you won't need these) | ||||
|   void setup() override; | ||||
| @@ -38,10 +24,9 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   void read_data_(); | ||||
|   void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f); | ||||
|  | ||||
|   BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; | ||||
|   uint8_t measurement_duration_; | ||||
|   uint8_t active_mtreg_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace bh1750 | ||||
|   | ||||
| @@ -2,31 +2,23 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_RESOLUTION, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_LUX, | ||||
|     CONF_MEASUREMENT_DURATION, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| CODEOWNERS = ["@OttoWinter"] | ||||
|  | ||||
| bh1750_ns = cg.esphome_ns.namespace("bh1750") | ||||
| BH1750Resolution = bh1750_ns.enum("BH1750Resolution") | ||||
| BH1750_RESOLUTIONS = { | ||||
|     4.0: BH1750Resolution.BH1750_RESOLUTION_4P0_LX, | ||||
|     1.0: BH1750Resolution.BH1750_RESOLUTION_1P0_LX, | ||||
|     0.5: BH1750Resolution.BH1750_RESOLUTION_0P5_LX, | ||||
| } | ||||
|  | ||||
| BH1750Sensor = bh1750_ns.class_( | ||||
|     "BH1750Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONF_MEASUREMENT_TIME = "measurement_time" | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         BH1750Sensor, | ||||
|         unit_of_measurement=UNIT_LUX, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
| @@ -34,15 +26,11 @@ CONFIG_SCHEMA = ( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BH1750Sensor), | ||||
|             cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum( | ||||
|                 BH1750_RESOLUTIONS, float=True | ||||
|             cv.Optional("resolution"): cv.invalid( | ||||
|                 "The 'resolution' option has been removed. The optimal value is now dynamically calculated." | ||||
|             ), | ||||
|             cv.Optional(CONF_MEASUREMENT_DURATION, default=69): cv.int_range( | ||||
|                 min=31, max=254 | ||||
|             ), | ||||
|             cv.Optional(CONF_MEASUREMENT_TIME): cv.invalid( | ||||
|                 "The 'measurement_time' option has been replaced with 'measurement_duration' in 1.18.0" | ||||
|             cv.Optional("measurement_duration"): cv.invalid( | ||||
|                 "The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated." | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
| @@ -52,10 +40,6 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await sensor.register_sensor(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_resolution(config[CONF_RESOLUTION])) | ||||
|     cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION])) | ||||
|   | ||||
| @@ -9,7 +9,7 @@ from esphome.const import ( | ||||
| ) | ||||
| from .. import binary_ns | ||||
|  | ||||
| BinaryFan = binary_ns.class_("BinaryFan", cg.Component) | ||||
| BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) | ||||
|  | ||||
| CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | ||||
|     { | ||||
| @@ -24,9 +24,8 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await fan.register_fan(var, config) | ||||
|  | ||||
|     fan_ = await fan.create_fan_state(config) | ||||
|     cg.add(var.set_fan(fan_)) | ||||
|     output_ = await cg.get_variable(config[CONF_OUTPUT]) | ||||
|     cg.add(var.set_output(output_)) | ||||
|  | ||||
|   | ||||
| @@ -6,59 +6,35 @@ namespace binary { | ||||
|  | ||||
| static const char *const TAG = "binary.fan"; | ||||
|  | ||||
| void binary::BinaryFan::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Fan '%s':", this->fan_->get_name().c_str()); | ||||
|   if (this->fan_->get_traits().supports_oscillation()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Oscillation: YES"); | ||||
|   } | ||||
|   if (this->fan_->get_traits().supports_direction()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Direction: YES"); | ||||
|   } | ||||
| } | ||||
| void BinaryFan::setup() { | ||||
|   auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); | ||||
|   this->fan_->set_traits(traits); | ||||
|   this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); | ||||
| } | ||||
| void BinaryFan::loop() { | ||||
|   if (!this->next_update_) { | ||||
|     return; | ||||
|   } | ||||
|   this->next_update_ = false; | ||||
|  | ||||
|   { | ||||
|     bool enable = this->fan_->state; | ||||
|     if (enable) | ||||
|       this->output_->turn_on(); | ||||
|     else | ||||
|       this->output_->turn_off(); | ||||
|     ESP_LOGD(TAG, "Setting binary state: %s", ONOFF(enable)); | ||||
|   } | ||||
|  | ||||
|   if (this->oscillating_ != nullptr) { | ||||
|     bool enable = this->fan_->oscillating; | ||||
|     if (enable) { | ||||
|       this->oscillating_->turn_on(); | ||||
|     } else { | ||||
|       this->oscillating_->turn_off(); | ||||
|     } | ||||
|     ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); | ||||
|   } | ||||
|  | ||||
|   if (this->direction_ != nullptr) { | ||||
|     bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; | ||||
|     if (enable) { | ||||
|       this->direction_->turn_on(); | ||||
|     } else { | ||||
|       this->direction_->turn_off(); | ||||
|     } | ||||
|     ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); | ||||
|   auto restore = this->restore_state_(); | ||||
|   if (restore.has_value()) { | ||||
|     restore->apply(*this); | ||||
|     this->write_state_(); | ||||
|   } | ||||
| } | ||||
| void BinaryFan::dump_config() { LOG_FAN("", "Binary Fan", this); } | ||||
| fan::FanTraits BinaryFan::get_traits() { | ||||
|   return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); | ||||
| } | ||||
| void BinaryFan::control(const fan::FanCall &call) { | ||||
|   if (call.get_state().has_value()) | ||||
|     this->state = *call.get_state(); | ||||
|   if (call.get_oscillating().has_value()) | ||||
|     this->oscillating = *call.get_oscillating(); | ||||
|   if (call.get_direction().has_value()) | ||||
|     this->direction = *call.get_direction(); | ||||
|  | ||||
| // We need a higher priority than the FanState component to make sure that the traits are set | ||||
| // when that component sets itself up. | ||||
| float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } | ||||
|   this->write_state_(); | ||||
|   this->publish_state(); | ||||
| } | ||||
| void BinaryFan::write_state_() { | ||||
|   this->output_->set_state(this->state); | ||||
|   if (this->oscillating_ != nullptr) | ||||
|     this->oscillating_->set_state(this->oscillating); | ||||
|   if (this->direction_ != nullptr) | ||||
|     this->direction_->set_state(this->direction == fan::FanDirection::REVERSE); | ||||
| } | ||||
|  | ||||
| }  // namespace binary | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -2,28 +2,29 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/output/binary_output.h" | ||||
| #include "esphome/components/fan/fan_state.h" | ||||
| #include "esphome/components/fan/fan.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace binary { | ||||
|  | ||||
| class BinaryFan : public Component { | ||||
| class BinaryFan : public Component, public fan::Fan { | ||||
|  public: | ||||
|   void set_fan(fan::FanState *fan) { fan_ = fan; } | ||||
|   void set_output(output::BinaryOutput *output) { output_ = output; } | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   void set_output(output::BinaryOutput *output) { this->output_ = output; } | ||||
|   void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } | ||||
|   void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } | ||||
|  | ||||
|   fan::FanTraits get_traits() override; | ||||
|  | ||||
|  protected: | ||||
|   fan::FanState *fan_; | ||||
|   void control(const fan::FanCall &call) override; | ||||
|   void write_state_(); | ||||
|  | ||||
|   output::BinaryOutput *output_; | ||||
|   output::BinaryOutput *oscillating_{nullptr}; | ||||
|   output::BinaryOutput *direction_{nullptr}; | ||||
|   bool next_update_{true}; | ||||
| }; | ||||
|  | ||||
| }  // namespace binary | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.cpp_generator import MockObjClass | ||||
| from esphome.cpp_helpers import setup_entity | ||||
| from esphome import automation, core | ||||
| from esphome.automation import Condition, maybe_simple_id | ||||
| @@ -7,7 +8,9 @@ from esphome.components import mqtt | ||||
| from esphome.const import ( | ||||
|     CONF_DELAY, | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_FILTERS, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_INVALID_COOLDOWN, | ||||
|     CONF_INVERTED, | ||||
| @@ -22,7 +25,6 @@ from esphome.const import ( | ||||
|     CONF_STATE, | ||||
|     CONF_TIMING, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_NAME, | ||||
|     CONF_MQTT_ID, | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
| @@ -315,7 +317,7 @@ def validate_multi_click_timing(value): | ||||
|     return timings | ||||
|  | ||||
|  | ||||
| device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") | ||||
| validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") | ||||
|  | ||||
|  | ||||
| BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( | ||||
| @@ -324,7 +326,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex | ||||
|         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( | ||||
|             mqtt.MQTTBinarySensorComponent | ||||
|         ), | ||||
|         cv.Optional(CONF_DEVICE_CLASS): device_class, | ||||
|         cv.Optional(CONF_DEVICE_CLASS): validate_device_class, | ||||
|         cv.Optional(CONF_FILTERS): validate_filters, | ||||
|         cv.Optional(CONF_ON_PRESS): automation.validate_automation( | ||||
|             { | ||||
| @@ -377,6 +379,39 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex | ||||
|     } | ||||
| ) | ||||
|  | ||||
| _UNDEF = object() | ||||
|  | ||||
|  | ||||
| def binary_sensor_schema( | ||||
|     class_: MockObjClass = _UNDEF, | ||||
|     *, | ||||
|     icon: str = _UNDEF, | ||||
|     entity_category: str = _UNDEF, | ||||
|     device_class: str = _UNDEF, | ||||
| ) -> cv.Schema: | ||||
|     schema = BINARY_SENSOR_SCHEMA | ||||
|     if class_ is not _UNDEF: | ||||
|         schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) | ||||
|     if icon is not _UNDEF: | ||||
|         schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) | ||||
|     if entity_category is not _UNDEF: | ||||
|         schema = schema.extend( | ||||
|             { | ||||
|                 cv.Optional( | ||||
|                     CONF_ENTITY_CATEGORY, default=entity_category | ||||
|                 ): cv.entity_category | ||||
|             } | ||||
|         ) | ||||
|     if device_class is not _UNDEF: | ||||
|         schema = schema.extend( | ||||
|             { | ||||
|                 cv.Optional( | ||||
|                     CONF_DEVICE_CLASS, default=device_class | ||||
|                 ): validate_device_class | ||||
|             } | ||||
|         ) | ||||
|     return schema | ||||
|  | ||||
|  | ||||
| async def setup_binary_sensor_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
| @@ -443,7 +478,7 @@ async def register_binary_sensor(var, config): | ||||
|  | ||||
|  | ||||
| async def new_binary_sensor(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await register_binary_sensor(var, config) | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -42,13 +42,15 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { | ||||
|   } | ||||
| } | ||||
| std::string BinarySensor::device_class() { return ""; } | ||||
| BinarySensor::BinarySensor(const std::string &name) : EntityBase(name), state(false) {} | ||||
| BinarySensor::BinarySensor() : BinarySensor("") {} | ||||
| BinarySensor::BinarySensor() : state(false) {} | ||||
| void BinarySensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } | ||||
| std::string BinarySensor::get_device_class() { | ||||
|   if (this->device_class_.has_value()) | ||||
|     return *this->device_class_; | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
|   return this->device_class(); | ||||
| #pragma GCC diagnostic pop | ||||
| } | ||||
| void BinarySensor::add_filter(Filter *filter) { | ||||
|   filter->parent_ = this; | ||||
| @@ -67,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) { | ||||
|   } | ||||
| } | ||||
| bool BinarySensor::has_state() const { return this->has_state_; } | ||||
| uint32_t BinarySensor::hash_base() { return 1210250844UL; } | ||||
| bool BinarySensor::is_status_binary_sensor() const { return false; } | ||||
|  | ||||
| }  // namespace binary_sensor | ||||
|   | ||||
| @@ -26,11 +26,6 @@ namespace binary_sensor { | ||||
| class BinarySensor : public EntityBase { | ||||
|  public: | ||||
|   explicit BinarySensor(); | ||||
|   /** Construct a binary sensor with the specified name | ||||
|    * | ||||
|    * @param name Name of this binary sensor. | ||||
|    */ | ||||
|   explicit BinarySensor(const std::string &name); | ||||
|  | ||||
|   /** Add a callback to be notified of state changes. | ||||
|    * | ||||
| @@ -74,12 +69,13 @@ class BinarySensor : public EntityBase { | ||||
|  | ||||
|   // ========== OVERRIDE METHODS ========== | ||||
|   // (You'll only need this when creating your own custom binary sensor) | ||||
|   /// Get the default device class for this sensor, or empty string for no default. | ||||
|   /** Override this to set the default device class. | ||||
|    * | ||||
|    * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) | ||||
|    */ | ||||
|   virtual std::string device_class(); | ||||
|  | ||||
|  protected: | ||||
|   uint32_t hash_base() override; | ||||
|  | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|   optional<std::string> device_class_{};  ///< Stores the override of the device class | ||||
|   Filter *filter_list_{nullptr}; | ||||
|   | ||||
| @@ -3,14 +3,12 @@ import esphome.config_validation as cv | ||||
|  | ||||
| from esphome.components import sensor, binary_sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_CHANNELS, | ||||
|     CONF_VALUE, | ||||
|     CONF_TYPE, | ||||
|     ICON_CHECK_CIRCLE_OUTLINE, | ||||
|     CONF_BINARY_SENSOR, | ||||
|     CONF_GROUP, | ||||
|     STATE_CLASS_NONE, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["binary_sensor"] | ||||
| @@ -33,12 +31,11 @@ entry = { | ||||
| CONFIG_SCHEMA = cv.typed_schema( | ||||
|     { | ||||
|         CONF_GROUP: sensor.sensor_schema( | ||||
|             BinarySensorMap, | ||||
|             icon=ICON_CHECK_CIRCLE_OUTLINE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|         ).extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(BinarySensorMap), | ||||
|                 cv.Required(CONF_CHANNELS): cv.All( | ||||
|                     cv.ensure_list(entry), cv.Length(min=1) | ||||
|                 ), | ||||
| @@ -50,9 +47,8 @@ CONFIG_SCHEMA = cv.typed_schema( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await sensor.register_sensor(var, config) | ||||
|  | ||||
|     constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] | ||||
|     cg.add(var.set_sensor_type(constant)) | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@ziceva"] | ||||
							
								
								
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| #include "bl0939.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0939 { | ||||
|  | ||||
| static const char *const TAG = "bl0939"; | ||||
|  | ||||
| // https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf | ||||
| // (unfortunatelly chinese, but the protocol can be understood with some translation tool) | ||||
| static const uint8_t BL0939_READ_COMMAND = 0x55;  // 0x5{A4,A3,A2,A1} | ||||
| static const uint8_t BL0939_FULL_PACKET = 0xAA; | ||||
| static const uint8_t BL0939_PACKET_HEADER = 0x55; | ||||
|  | ||||
| static const uint8_t BL0939_WRITE_COMMAND = 0xA5;  // 0xA{A4,A3,A2,A1} | ||||
| static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10; | ||||
| static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E; | ||||
| static const uint8_t BL0939_REG_MODE = 0x18; | ||||
| static const uint8_t BL0939_REG_SOFT_RESET = 0x19; | ||||
| static const uint8_t BL0939_REG_USR_WRPROT = 0x1A; | ||||
| static const uint8_t BL0939_REG_TPS_CTRL = 0x1B; | ||||
|  | ||||
| const uint8_t BL0939_INIT[6][6] = { | ||||
|     // Reset to default | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33}, | ||||
|     // Enable User Operation Write | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB}, | ||||
|     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32}, | ||||
|     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9}, | ||||
|     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16}, | ||||
|     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||
|     {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}}; | ||||
|  | ||||
| void BL0939::loop() { | ||||
|   DataPacket buffer; | ||||
|   if (!this->available()) { | ||||
|     return; | ||||
|   } | ||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||
|     if (validate_checksum(&buffer)) { | ||||
|       received_package_(&buffer); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||
|     while (read() >= 0) | ||||
|       ; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool BL0939::validate_checksum(const DataPacket *data) { | ||||
|   uint8_t checksum = BL0939_READ_COMMAND; | ||||
|   // Whole package but checksum | ||||
|   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { | ||||
|     checksum += data->raw[i]; | ||||
|   } | ||||
|   checksum ^= 0xFF; | ||||
|   if (checksum != data->checksum) { | ||||
|     ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||
|   } | ||||
|   return checksum == data->checksum; | ||||
| } | ||||
|  | ||||
| void BL0939::update() { | ||||
|   this->flush(); | ||||
|   this->write_byte(BL0939_READ_COMMAND); | ||||
|   this->write_byte(BL0939_FULL_PACKET); | ||||
| } | ||||
|  | ||||
| void BL0939::setup() { | ||||
|   for (auto *i : BL0939_INIT) { | ||||
|     this->write_array(i, 6); | ||||
|     delay(1); | ||||
|   } | ||||
|   this->flush(); | ||||
| } | ||||
|  | ||||
| void BL0939::received_package_(const DataPacket *data) const { | ||||
|   // Bad header | ||||
|   if (data->frame_header != BL0939_PACKET_HEADER) { | ||||
|     ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; | ||||
|   float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_; | ||||
|   float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_; | ||||
|   float a_watt = (float) to_int32_t(data->a_watt) / power_reference_; | ||||
|   float b_watt = (float) to_int32_t(data->b_watt) / power_reference_; | ||||
|   int32_t cfa_cnt = to_int32_t(data->cfa_cnt); | ||||
|   int32_t cfb_cnt = to_int32_t(data->cfb_cnt); | ||||
|   float a_energy_consumption = (float) cfa_cnt / energy_reference_; | ||||
|   float b_energy_consumption = (float) cfb_cnt / energy_reference_; | ||||
|   float total_energy_consumption = a_energy_consumption + b_energy_consumption; | ||||
|  | ||||
|   if (voltage_sensor_ != nullptr) { | ||||
|     voltage_sensor_->publish_state(v_rms); | ||||
|   } | ||||
|   if (current_sensor_1_ != nullptr) { | ||||
|     current_sensor_1_->publish_state(ia_rms); | ||||
|   } | ||||
|   if (current_sensor_2_ != nullptr) { | ||||
|     current_sensor_2_->publish_state(ib_rms); | ||||
|   } | ||||
|   if (power_sensor_1_ != nullptr) { | ||||
|     power_sensor_1_->publish_state(a_watt); | ||||
|   } | ||||
|   if (power_sensor_2_ != nullptr) { | ||||
|     power_sensor_2_->publish_state(b_watt); | ||||
|   } | ||||
|   if (energy_sensor_1_ != nullptr) { | ||||
|     energy_sensor_1_->publish_state(a_energy_consumption); | ||||
|   } | ||||
|   if (energy_sensor_2_ != nullptr) { | ||||
|     energy_sensor_2_->publish_state(b_energy_consumption); | ||||
|   } | ||||
|   if (energy_sensor_sum_ != nullptr) { | ||||
|     energy_sensor_sum_->publish_state(total_energy_consumption); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms, | ||||
|            ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); | ||||
| } | ||||
|  | ||||
| void BL0939::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | ||||
|   ESP_LOGCONFIG(TAG, "BL0939:"); | ||||
|   LOG_SENSOR("", "Voltage", this->voltage_sensor_); | ||||
|   LOG_SENSOR("", "Current 1", this->current_sensor_1_); | ||||
|   LOG_SENSOR("", "Current 2", this->current_sensor_2_); | ||||
|   LOG_SENSOR("", "Power 1", this->power_sensor_1_); | ||||
|   LOG_SENSOR("", "Power 2", this->power_sensor_2_); | ||||
|   LOG_SENSOR("", "Energy 1", this->energy_sensor_1_); | ||||
|   LOG_SENSOR("", "Energy 2", this->energy_sensor_2_); | ||||
|   LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_); | ||||
| } | ||||
|  | ||||
| uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||
|  | ||||
| int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||
|  | ||||
| }  // namespace bl0939 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0939 { | ||||
|  | ||||
| // https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf | ||||
| // (unfortunatelly chinese, but the formulas can be easily understood) | ||||
| // Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm) | ||||
| // and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm) | ||||
| // as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V) | ||||
| static const float BL0939_IREF = 324004 * 1 / 1.218; | ||||
| static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51)); | ||||
| static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51)); | ||||
| static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51)); | ||||
|  | ||||
| struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t m; | ||||
|   uint8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t m; | ||||
|   int8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| // Caveat: All these values are big endian (low - middle - high) | ||||
|  | ||||
| union DataPacket {  // NOLINT(altera-struct-pack-align) | ||||
|   uint8_t raw[35]; | ||||
|   struct { | ||||
|     uint8_t frame_header;  // 0x55 according to docs | ||||
|     ube24_t ia_fast_rms; | ||||
|     ube24_t ia_rms; | ||||
|     ube24_t ib_rms; | ||||
|     ube24_t v_rms; | ||||
|     ube24_t ib_fast_rms; | ||||
|     sbe24_t a_watt; | ||||
|     sbe24_t b_watt; | ||||
|     sbe24_t cfa_cnt; | ||||
|     sbe24_t cfb_cnt; | ||||
|     ube16_t tps1; | ||||
|     uint8_t RESERVED1;  // value of 0x00 | ||||
|     ube16_t tps2; | ||||
|     uint8_t RESERVED2;  // value of 0x00 | ||||
|     uint8_t checksum;   // checksum | ||||
|   }; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| class BL0939 : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||
|   void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } | ||||
|   void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } | ||||
|   void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } | ||||
|   void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } | ||||
|   void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; } | ||||
|   void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; } | ||||
|   void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; } | ||||
|  | ||||
|   void loop() override; | ||||
|  | ||||
|   void update() override; | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *voltage_sensor_; | ||||
|   sensor::Sensor *current_sensor_1_; | ||||
|   sensor::Sensor *current_sensor_2_; | ||||
|   // NB This may be negative as the circuits is seemingly able to measure | ||||
|   // power in both directions | ||||
|   sensor::Sensor *power_sensor_1_; | ||||
|   sensor::Sensor *power_sensor_2_; | ||||
|   sensor::Sensor *energy_sensor_1_; | ||||
|   sensor::Sensor *energy_sensor_2_; | ||||
|   sensor::Sensor *energy_sensor_sum_; | ||||
|  | ||||
|   // Divide by this to turn into Watt | ||||
|   float power_reference_ = BL0939_PREF; | ||||
|   // Divide by this to turn into Volt | ||||
|   float voltage_reference_ = BL0939_UREF; | ||||
|   // Divide by this to turn into Ampere | ||||
|   float current_reference_ = BL0939_IREF; | ||||
|   // Divide by this to turn into kWh | ||||
|   float energy_reference_ = BL0939_EREF; | ||||
|  | ||||
|   static uint32_t to_uint32_t(ube24_t input); | ||||
|  | ||||
|   static int32_t to_int32_t(sbe24_t input); | ||||
|  | ||||
|   static bool validate_checksum(const DataPacket *data); | ||||
|  | ||||
|   void received_package_(const DataPacket *data) const; | ||||
| }; | ||||
| }  // namespace bl0939 | ||||
| }  // namespace esphome | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user