mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Compare commits
	
		
			330 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fd64585f99 | ||
|  | 077cce9848 | ||
|  | bd87e56bc7 | ||
|  | 58235049e3 | ||
|  | 29ed3c20af | ||
|  | 08aae39ea4 | ||
|  | 03fd114371 | ||
|  | 918650f15a | ||
|  | 287f65cbaf | ||
|  | f18c70a256 | ||
|  | 6fb490f49b | ||
|  | 66cf7c3a3b | ||
|  | f29021b5ef | ||
|  | 7549ca4d39 | ||
|  | 33e7a2101b | ||
|  | 59a216bfcb | ||
|  | 09d89000ad | ||
|  | b6c9ece0e6 | ||
|  | 7169556722 | ||
|  | f6e4c0cb52 | ||
|  | f3634edc22 | ||
|  | a609343cb6 | ||
|  | 5528c3c765 | ||
|  | 0d805355f5 | ||
|  | 99f48ae51c | ||
|  | 25e4aafd71 | ||
|  | 4f2d54be4e | ||
|  | 249cd7415b | ||
|  | 78d780105b | ||
|  | 466d4522bc | ||
|  | e462217500 | ||
|  | f1bce262ed | ||
|  | 7ed7e7ad26 | ||
|  | 08b8454555 | ||
|  | 0119e17f04 | ||
|  | c3f40de844 | ||
|  | 7dd829cfca | ||
|  | da19673f51 | ||
|  | f5e32d03d0 | ||
|  | f3b69383fd | ||
|  | aba72809d3 | ||
|  | 85205a28d2 | ||
|  | 285e006637 | ||
|  | 5647f36900 | ||
|  | 1e9309ffff | ||
|  | ce8a6a6c43 | ||
|  | dfb4b31bf9 | ||
|  | 31b1b50ad9 | ||
|  | 3377080272 | ||
|  | d65ad69338 | ||
|  | dfa69173ea | ||
|  | f44615cc8d | ||
|  | bda4769bd3 | ||
|  | 14b057f54e | ||
|  | e26b5874d7 | ||
|  | 00f22e5c36 | ||
|  | 51e080c2d3 | ||
|  | 3c18558003 | ||
|  | 7394cbf773 | ||
|  | 1577a46efd | ||
|  | e212ed024d | ||
|  | 5fdd90c71a | ||
|  | 6929bdb415 | ||
|  | 2c85ba037e | ||
|  | 2440bbdceb | ||
|  | 3ac8eb7696 | ||
|  | 6a478b9070 | ||
|  | a32a1d11fb | ||
|  | daeb8ef88c | ||
|  | febee437d6 | ||
|  | de2f475dbd | ||
|  | fa3ec6f732 | ||
|  | e490aec6b4 | ||
|  | 8da8095a6a | ||
|  | ab14c0cd72 | ||
|  | 917deac7cb | ||
|  | 3d21adecd3 | ||
|  | 5b023f9369 | ||
|  | 6c2ce5cacf | ||
|  | d23e25f099 | ||
|  | 9b78098eec | ||
|  | 7e5b82c5f3 | ||
|  | 2864e989bd | ||
|  | 6efe346cc5 | ||
|  | f2f6c597ef | ||
|  | b91b12d77a | ||
|  | 7f567bdfbe | ||
|  | f2de8df556 | ||
|  | 1c67a61945 | ||
|  | 77141d3e83 | ||
|  | f592f79bce | ||
|  | 6edbb94529 | ||
|  | d37eb59fd7 | ||
|  | e2b3617df3 | ||
|  | e1c851cab8 | ||
|  | 146b067d62 | ||
|  | 5b15827009 | ||
|  | 0de79ba291 | ||
|  | e3aaf6a144 | ||
|  | 78ffeb30fb | ||
|  | 2c1927fd12 | ||
|  | c6ae1a5909 | ||
|  | 9c712744be | ||
|  | ae50a09b4e | ||
|  | 1ea80594c6 | ||
|  | 8500323d39 | ||
|  | 6f7db2f5f7 | ||
|  | 9922c65912 | ||
|  | f2469077d9 | ||
|  | 742eca92d8 | ||
|  | 548913b471 | ||
|  | a05c5ea240 | ||
|  | 8e8a2bde95 | ||
|  | 80265a6bd2 | ||
|  | 87e9a7a1bd | ||
|  | 3aedfe8be3 | ||
|  | 7f2cc47ed6 | ||
|  | a5542e0d2b | ||
|  | 66afe4a9be | ||
|  | 0ae9009e41 | ||
|  | 0b2f5fcd7e | ||
|  | 7a2887e2ed | ||
|  | cd2d3f061d | ||
|  | 73f5d01c2d | ||
|  | 0938609f7a | ||
|  | 77203f0cb4 | ||
|  | 040130e357 | ||
|  | 85959e3004 | ||
|  | a809a13729 | ||
|  | 3b6ff615e8 | ||
|  | 05216db5f0 | ||
|  | 9f668b0c4b | ||
|  | 6a239f4d1c | ||
|  | ffb0e854b6 | ||
|  | 6fbd0e3385 | ||
|  | 426511e78d | ||
|  | 97d91fee85 | ||
|  | 0f4b54aa82 | ||
|  | 1706a69fad | ||
|  | e23d66a8cf | ||
|  | 46101fd830 | ||
|  | e988905c2f | ||
|  | abb57f08f5 | ||
|  | ca2fe994a1 | ||
|  | 03def13917 | ||
|  | 63f100a8ca | ||
|  | ea4e5fd7bd | ||
|  | 12e9c5e60e | ||
|  | 3d82c5baf7 | ||
|  | 6f5e36ffc3 | ||
|  | 118b1d8593 | ||
|  | 319ba4a504 | ||
|  | ae8336c268 | ||
|  | 1b38518c63 | ||
|  | c00977df54 | ||
|  | 255b5a3abd | ||
|  | dd732dd155 | ||
|  | 22fec4329f | ||
|  | 8f1c4634ec | ||
|  | c15f1a9be8 | ||
|  | 11b53096a6 | ||
|  | 6a18367949 | ||
|  | a59b1494d8 | ||
|  | e6ce5c58d1 | ||
|  | 0f87e7508b | ||
|  | 862bbb7fe1 | ||
|  | 020cea80b2 | ||
|  | 9c146a7070 | ||
|  | afbd3f77af | ||
|  | 1e1fefbd0a | ||
|  | 1a2057df30 | ||
|  | 70cb1793f3 | ||
|  | 3bdd351d49 | ||
|  | b0ea3f57de | ||
|  | c9312d5c27 | ||
|  | 33fea90c19 | ||
|  | 25f3b6a959 | ||
|  | e993312640 | ||
|  | 85babe85e4 | ||
|  | 0266c897c9 | ||
|  | bda7676e3a | ||
|  | 57e98ec3fc | ||
|  | 09b2ad071b | ||
|  | fdecda3d65 | ||
|  | a0922bc8b0 | ||
|  | f25af18655 | ||
|  | 5db07c2d70 | ||
|  | 40823df7bc | ||
|  | 5e1019a6fa | ||
|  | f3cdbd0a05 | ||
|  | ddf1b67e49 | ||
|  | b4d9fddd07 | ||
|  | 25f03074ab | ||
|  | 590f6ff70b | ||
|  | a33ed5e47b | ||
|  | c11a9bb97f | ||
|  | acef2085d9 | ||
|  | 865663ce5f | ||
|  | ae010fd6f1 | ||
|  | 91a10d0e36 | ||
|  | d5c36eaf2a | ||
|  | 85f1019d90 | ||
|  | bfeade1e2b | ||
|  | b134d42e3b | ||
|  | b3b65316f0 | ||
|  | b61cec8e77 | ||
|  | 24243fb22c | ||
|  | ba6c8c87c2 | ||
|  | f5774cc138 | ||
|  | 6d09e68b2e | ||
|  | fe9db75c27 | ||
|  | 2b832e9ee8 | ||
|  | 661e9f9991 | ||
|  | 39e23c323d | ||
|  | bdfbac0301 | ||
|  | 9646653e57 | ||
|  | c6c202e4f7 | ||
|  | 62f73c768e | ||
|  | cd1215347e | ||
|  | b8353b3117 | ||
|  | 5d3574c81f | ||
|  | 364e5ffd79 | ||
|  | c38c2a1daf | ||
|  | 7e2ccb7bc3 | ||
|  | 14d76e9e4e | ||
|  | f2e0a412db | ||
|  | 6943b1d985 | ||
|  | 18062d154f | ||
|  | 2b0b82b2fb | ||
|  | 3e1c8f37c5 | ||
|  | 236ca12d3e | ||
|  | 42f1b61e31 | ||
|  | 708f8a95e5 | ||
|  | 10ca86ae8d | ||
|  | 22056e0809 | ||
|  | fe4857fabb | ||
|  | 3054c2bc29 | ||
|  | b190f37ae7 | ||
|  | 28454b8219 | ||
|  | 332f52e149 | ||
|  | ae1f54d398 | ||
|  | 2b3e7f38d2 | ||
|  | 5510ece6ac | ||
|  | e19a85b523 | ||
|  | cf02a08209 | ||
|  | 90e8c12df1 | ||
|  | 42bf5840c9 | ||
|  | 47817485e7 | ||
|  | ded98ff705 | ||
|  | 7f8ca5ddef | ||
|  | 1b0ca3360e | ||
|  | 66263b40e1 | ||
|  | 6d5e41ef7f | ||
|  | fedfda6c29 | ||
|  | 6e2088f836 | ||
|  | 7a82379c88 | ||
|  | c983581b6c | ||
|  | f0ac61f247 | ||
|  | 85420b0606 | ||
|  | 00230f7cc6 | ||
|  | 63a87a5ef3 | ||
|  | d75ae357c2 | ||
|  | 88d223d03a | ||
|  | b927b29a0a | ||
|  | 72ec9b672e | ||
|  | 8f49b1da54 | ||
|  | 9ff6f344ab | ||
|  | 0e703ddbba | ||
|  | 2175c2909b | ||
|  | cbdb9d4a56 | ||
|  | baa010583e | ||
|  | 8e9a68a107 | ||
|  | 4c688a4b00 | ||
|  | 9eef281895 | ||
|  | 8a15c18066 | ||
|  | 5bb69a968c | ||
|  | 7c02f2f10a | ||
|  | fe07c34246 | ||
|  | c652aa375a | ||
|  | 9fb254fdc2 | ||
|  | 3df4dbd3a6 | ||
|  | 6372099df3 | ||
|  | 8d8fcfeda2 | ||
|  | 0f356fcc79 | ||
|  | aec60d122b | ||
|  | c10f68ef0c | ||
|  | bcc424afed | ||
|  | be2c859df3 | ||
|  | 59f728488e | ||
|  | 04a0de556d | ||
|  | 1f13d44c1b | ||
|  | 9ebfa9aaa8 | ||
|  | 6bc9ed0810 | ||
|  | 9b6e8b4b41 | ||
|  | cad747c672 | ||
|  | 660adccda3 | ||
|  | 51fbc4f7a3 | ||
|  | 2cc5e24b38 | ||
|  | 3afa73b449 | ||
|  | dcf2697a2a | ||
|  | 6a11700a6b | ||
|  | 9bd9b043c8 | ||
|  | cb602c9b1a | ||
|  | b54beb357a | ||
|  | 6abc2efd96 | ||
|  | be51093a7e | ||
|  | 52219c4dcc | ||
|  | 590cae13c0 | ||
|  | e15429b0f5 | ||
|  | b5cc668a45 | ||
|  | a1b0ae78e0 | ||
|  | fcc8a809e6 | ||
|  | 48474c0f8c | ||
|  | 9f9c95dd09 | ||
|  | a74fcbc8b6 | ||
|  | c8b898f9c5 | ||
|  | 81bf2688b4 | ||
|  | 87d2c9868f | ||
|  | 5ca407e27c | ||
|  | 5bbc2ab482 | ||
|  | 309e8b4c92 | ||
|  | eee2987c99 | ||
|  | 061e55f8c5 | ||
|  | 56334b7832 | ||
|  | a4b7e0c700 | ||
|  | 84ad7ee0e4 | ||
|  | d006008539 | ||
|  | 6bb1e4c9c0 | ||
|  | 82bdb08884 | ||
|  | b709ff84c3 | 
| @@ -221,6 +221,146 @@ This document provides essential context for AI models interacting with this pro | ||||
|     *   **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. | ||||
|     *   **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations. | ||||
|     *   **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization. | ||||
|     *   **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage. | ||||
|  | ||||
|         **STL Container Guidelines:** | ||||
|  | ||||
|         ESPHome runs on embedded systems with limited resources. Choose containers carefully: | ||||
|  | ||||
|         1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time. | ||||
|            ```cpp | ||||
|            // Bad - generates STL realloc code | ||||
|            std::vector<int> values; | ||||
|  | ||||
|            // Good - no dynamic allocation | ||||
|            std::array<int, MAX_VALUES> values; | ||||
|            ``` | ||||
|            Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration. | ||||
|  | ||||
|            **For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead. | ||||
|  | ||||
|            > **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead. | ||||
|  | ||||
|            ```cpp | ||||
|            // Bad - STL overhead for simple byte buffer | ||||
|            std::vector<uint8_t> buffer; | ||||
|            buffer.resize(256); | ||||
|  | ||||
|            // Good - minimal overhead, single allocation | ||||
|            std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256); | ||||
|            // Or if size is constant: | ||||
|            std::array<uint8_t, 256> buffer; | ||||
|            ``` | ||||
|  | ||||
|         2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface. | ||||
|            ```cpp | ||||
|            // Bad - generates STL realloc code (_M_realloc_insert) | ||||
|            std::vector<ServiceRecord> services; | ||||
|            services.reserve(5);  // Still includes reallocation machinery | ||||
|  | ||||
|            // Good - compile-time fixed size, stack allocated, no reallocation machinery | ||||
|            StaticVector<ServiceRecord, MAX_SERVICES> services;  // Allocates all MAX_SERVICES on stack | ||||
|            services.push_back(record1);  // Tracks count but all slots allocated | ||||
|            ``` | ||||
|            Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration. | ||||
|            Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code. | ||||
|  | ||||
|         3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization. | ||||
|            ```cpp | ||||
|            // Bad - generates STL realloc code (_M_realloc_insert) | ||||
|            std::vector<TxtRecord> txt_records; | ||||
|            txt_records.reserve(5);  // Still includes reallocation machinery | ||||
|  | ||||
|            // Good - runtime size, single allocation, no reallocation machinery | ||||
|            FixedVector<TxtRecord> txt_records; | ||||
|            txt_records.init(record_count);  // Initialize with exact size at runtime | ||||
|            ``` | ||||
|            **Benefits:** | ||||
|            - Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance) | ||||
|            - Single allocation, no upper bound needed | ||||
|            - No reallocation overhead | ||||
|            - Compatible with protobuf code generation when using `[(fixed_vector) = true]` option | ||||
|  | ||||
|         4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`. | ||||
|            ```cpp | ||||
|            // Bad - 2KB+ overhead for red-black tree/hash table | ||||
|            std::map<std::string, int> small_lookup; | ||||
|            std::unordered_map<int, std::string> tiny_map; | ||||
|  | ||||
|            // Good - simple struct with linear search (std::vector is fine) | ||||
|            struct LookupEntry { | ||||
|              const char *key; | ||||
|              int value; | ||||
|            }; | ||||
|            std::vector<LookupEntry> small_lookup = { | ||||
|              {"key1", 10}, | ||||
|              {"key2", 20}, | ||||
|              {"key3", 30}, | ||||
|            }; | ||||
|            // Or std::array if size is compile-time constant: | ||||
|            // std::array<LookupEntry, 3> small_lookup = {{ ... }}; | ||||
|            ``` | ||||
|            Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise. | ||||
|  | ||||
|         5. **Detection:** Look for these patterns in compiler output: | ||||
|            - Large code sections with STL symbols (vector, map, set) | ||||
|            - `alloc`, `realloc`, `dealloc` in symbol names | ||||
|            - `_M_realloc_insert`, `_M_default_append` (vector reallocation) | ||||
|            - Red-black tree code (`rb_tree`, `_Rb_tree`) | ||||
|            - Hash table infrastructure (`unordered_map`, `hash`) | ||||
|  | ||||
|         **When to optimize:** | ||||
|         - Core components (API, network, logger) | ||||
|         - Widely-used components (mdns, wifi, ble) | ||||
|         - Components causing flash size complaints | ||||
|  | ||||
|         **When not to optimize:** | ||||
|         - Single-use niche components | ||||
|         - Code where readability matters more than bytes | ||||
|         - Already using appropriate containers | ||||
|  | ||||
|     *   **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. | ||||
|  | ||||
|         **Bad Pattern (Module-Level Globals):** | ||||
|         ```python | ||||
|         # Don't do this - state persists between compilation runs | ||||
|         _component_state = [] | ||||
|         _use_feature = None | ||||
|  | ||||
|         def enable_feature(): | ||||
|             global _use_feature | ||||
|             _use_feature = True | ||||
|         ``` | ||||
|  | ||||
|         **Good Pattern (CORE.data with Helpers):** | ||||
|         ```python | ||||
|         from esphome.core import CORE | ||||
|  | ||||
|         # Keys for CORE.data storage | ||||
|         COMPONENT_STATE_KEY = "my_component_state" | ||||
|         USE_FEATURE_KEY = "my_component_use_feature" | ||||
|  | ||||
|         def _get_component_state() -> list: | ||||
|             """Get component state from CORE.data.""" | ||||
|             return CORE.data.setdefault(COMPONENT_STATE_KEY, []) | ||||
|  | ||||
|         def _get_use_feature() -> bool | None: | ||||
|             """Get feature flag from CORE.data.""" | ||||
|             return CORE.data.get(USE_FEATURE_KEY) | ||||
|  | ||||
|         def _set_use_feature(value: bool) -> None: | ||||
|             """Set feature flag in CORE.data.""" | ||||
|             CORE.data[USE_FEATURE_KEY] = value | ||||
|  | ||||
|         def enable_feature(): | ||||
|             _set_use_feature(True) | ||||
|         ``` | ||||
|  | ||||
|         **Why this matters:** | ||||
|         - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec | ||||
|         - `CORE.data` automatically clears between runs | ||||
|         - Typed helper functions provide better IDE support and maintainability | ||||
|         - Encapsulation makes state management explicit and testable | ||||
|  | ||||
| *   **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys. | ||||
|  | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| 049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f | ||||
| 3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| [run] | ||||
| omit = | ||||
|     esphome/components/* | ||||
|     esphome/analyze_memory/* | ||||
|     tests/integration/* | ||||
|   | ||||
							
								
								
									
										22
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -53,6 +53,7 @@ jobs: | ||||
|               'new-target-platform', | ||||
|               'merging-to-release', | ||||
|               'merging-to-beta', | ||||
|               'chained-pr', | ||||
|               'core', | ||||
|               'small-pr', | ||||
|               'dashboard', | ||||
| @@ -140,6 +141,8 @@ jobs: | ||||
|                 labels.add('merging-to-release'); | ||||
|               } else if (baseRef === 'beta') { | ||||
|                 labels.add('merging-to-beta'); | ||||
|               } else if (baseRef !== 'dev') { | ||||
|                 labels.add('chained-pr'); | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
| @@ -413,7 +416,7 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             // Generate review messages | ||||
|             function generateReviewMessages(finalLabels) { | ||||
|             function generateReviewMessages(finalLabels, originalLabelCount) { | ||||
|               const messages = []; | ||||
|               const prAuthor = context.payload.pull_request.user.login; | ||||
|  | ||||
| @@ -427,15 +430,15 @@ jobs: | ||||
|                   .reduce((sum, file) => sum + (file.deletions || 0), 0); | ||||
|                 const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); | ||||
|  | ||||
|                 const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|                 const tooManyLabels = originalLabelCount > MAX_LABELS; | ||||
|                 const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; | ||||
|  | ||||
|                 let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; | ||||
|  | ||||
|                 if (tooManyLabels && tooManyChanges) { | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; | ||||
|                 } else if (tooManyLabels) { | ||||
|                   message += `This PR affects ${finalLabels.length} different components/areas.`; | ||||
|                   message += `This PR affects ${originalLabelCount} different components/areas.`; | ||||
|                 } else { | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; | ||||
|                 } | ||||
| @@ -463,8 +466,8 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             // Handle reviews | ||||
|             async function handleReviews(finalLabels) { | ||||
|               const reviewMessages = generateReviewMessages(finalLabels); | ||||
|             async function handleReviews(finalLabels, originalLabelCount) { | ||||
|               const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); | ||||
|               const hasReviewableLabels = finalLabels.some(label => | ||||
|                 ['too-big', 'needs-codeowners'].includes(label) | ||||
|               ); | ||||
| @@ -528,8 +531,8 @@ jobs: | ||||
|             const apiData = await fetchApiData(); | ||||
|             const baseRef = context.payload.pull_request.base.ref; | ||||
|  | ||||
|             // Early exit for non-dev branches | ||||
|             if (baseRef !== 'dev') { | ||||
|             // Early exit for release and beta branches only | ||||
|             if (baseRef === 'release' || baseRef === 'beta') { | ||||
|               const branchLabels = await detectMergeBranch(); | ||||
|               const finalLabels = Array.from(branchLabels); | ||||
|  | ||||
| @@ -624,6 +627,7 @@ jobs: | ||||
|  | ||||
|             // Handle too many labels (only for non-mega PRs) | ||||
|             const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|             const originalLabelCount = finalLabels.length; | ||||
|  | ||||
|             if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { | ||||
|               finalLabels = ['too-big']; | ||||
| @@ -632,7 +636,7 @@ jobs: | ||||
|             console.log('Computed labels:', finalLabels.join(', ')); | ||||
|  | ||||
|             // Handle reviews | ||||
|             await handleReviews(finalLabels); | ||||
|             await handleReviews(finalLabels, originalLabelCount); | ||||
|  | ||||
|             // Apply labels | ||||
|             if (finalLabels.length > 0) { | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							| @@ -62,7 +62,7 @@ jobs: | ||||
|         run: git diff | ||||
|       - if: failure() | ||||
|         name: Archive artifacts | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: generated-proto-files | ||||
|           path: | | ||||
|   | ||||
							
								
								
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| --- | ||||
| name: Memory Impact Comment (Forks) | ||||
|  | ||||
| on: | ||||
|   workflow_run: | ||||
|     workflows: ["CI"] | ||||
|     types: [completed] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   pull-requests: write | ||||
|   actions: read | ||||
|  | ||||
| jobs: | ||||
|   memory-impact-comment: | ||||
|     name: Post memory impact comment (fork PRs only) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     # Only run for PRs from forks that had successful CI runs | ||||
|     if: > | ||||
|       github.event.workflow_run.event == 'pull_request' && | ||||
|       github.event.workflow_run.conclusion == 'success' && | ||||
|       github.event.workflow_run.head_repository.full_name != github.repository | ||||
|     env: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     steps: | ||||
|       - name: Get PR details | ||||
|         id: pr | ||||
|         run: | | ||||
|           # Get PR details by searching for PR with matching head SHA | ||||
|           # The workflow_run.pull_requests field is often empty for forks | ||||
|           # Use paginate to handle repos with many open PRs | ||||
|           head_sha="${{ github.event.workflow_run.head_sha }}" | ||||
|           pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \ | ||||
|             --jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \ | ||||
|             | head -n 1) | ||||
|  | ||||
|           if [ -z "$pr_data" ]; then | ||||
|             echo "No PR found for SHA $head_sha, skipping" | ||||
|             echo "skip=true" >> "$GITHUB_OUTPUT" | ||||
|             exit 0 | ||||
|           fi | ||||
|  | ||||
|           pr_number=$(echo "$pr_data" | jq -r '.number') | ||||
|           base_ref=$(echo "$pr_data" | jq -r '.base_ref') | ||||
|  | ||||
|           echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" | ||||
|           echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" | ||||
|           echo "Found PR #$pr_number targeting base branch: $base_ref" | ||||
|  | ||||
|       - name: Check out code from base repository | ||||
|         if: steps.pr.outputs.skip != 'true' | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         with: | ||||
|           # Always check out from the base repository (esphome/esphome), never from forks | ||||
|           # Use the PR's target branch to ensure we run trusted code from the main repo | ||||
|           repository: ${{ github.repository }} | ||||
|           ref: ${{ steps.pr.outputs.base_ref }} | ||||
|  | ||||
|       - name: Restore Python | ||||
|         if: steps.pr.outputs.skip != 'true' | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|           cache-key: ${{ hashFiles('.cache-key') }} | ||||
|  | ||||
|       - name: Download memory analysis artifacts | ||||
|         if: steps.pr.outputs.skip != 'true' | ||||
|         run: | | ||||
|           run_id="${{ github.event.workflow_run.id }}" | ||||
|           echo "Downloading artifacts from workflow run $run_id" | ||||
|  | ||||
|           mkdir -p memory-analysis | ||||
|  | ||||
|           # Download target analysis artifact | ||||
|           if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then | ||||
|             echo "Downloaded memory-analysis-target artifact." | ||||
|           else | ||||
|             echo "No memory-analysis-target artifact found." | ||||
|           fi | ||||
|  | ||||
|           # Download PR analysis artifact | ||||
|           if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then | ||||
|             echo "Downloaded memory-analysis-pr artifact." | ||||
|           else | ||||
|             echo "No memory-analysis-pr artifact found." | ||||
|           fi | ||||
|  | ||||
|       - name: Check if artifacts exist | ||||
|         id: check | ||||
|         if: steps.pr.outputs.skip != 'true' | ||||
|         run: | | ||||
|           if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then | ||||
|             echo "found=true" >> "$GITHUB_OUTPUT" | ||||
|           else | ||||
|             echo "found=false" >> "$GITHUB_OUTPUT" | ||||
|             echo "Memory analysis artifacts not found, skipping comment" | ||||
|           fi | ||||
|  | ||||
|       - name: Post or update PR comment | ||||
|         if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true' | ||||
|         env: | ||||
|           PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           # Pass PR number and JSON file paths directly to Python script | ||||
|           # Let Python parse the JSON to avoid shell injection risks | ||||
|           # The script will validate and sanitize all inputs | ||||
|           python script/ci_memory_impact_comment.py \ | ||||
|             --pr-number "$PR_NUMBER" \ | ||||
|             --target-json ./memory-analysis/memory-analysis-target.json \ | ||||
|             --pr-json ./memory-analysis/memory-analysis-pr.json | ||||
							
								
								
									
										597
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										597
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -114,7 +114,6 @@ jobs: | ||||
|       matrix: | ||||
|         python-version: | ||||
|           - "3.11" | ||||
|           - "3.12" | ||||
|           - "3.13" | ||||
|         os: | ||||
|           - ubuntu-latest | ||||
| @@ -126,12 +125,8 @@ jobs: | ||||
|           # version used for docker images on Windows and macOS | ||||
|           - python-version: "3.13" | ||||
|             os: windows-latest | ||||
|           - python-version: "3.12" | ||||
|             os: windows-latest | ||||
|           - python-version: "3.13" | ||||
|             os: macOS-latest | ||||
|           - python-version: "3.12" | ||||
|             os: macOS-latest | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: | ||||
|       - common | ||||
| @@ -175,10 +170,17 @@ jobs: | ||||
|     outputs: | ||||
|       integration-tests: ${{ steps.determine.outputs.integration-tests }} | ||||
|       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} | ||||
|       clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} | ||||
|       python-linters: ${{ steps.determine.outputs.python-linters }} | ||||
|       changed-components: ${{ steps.determine.outputs.changed-components }} | ||||
|       changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} | ||||
|       directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} | ||||
|       component-test-count: ${{ steps.determine.outputs.component-test-count }} | ||||
|       changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }} | ||||
|       memory_impact: ${{ steps.determine.outputs.memory-impact }} | ||||
|       cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} | ||||
|       cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} | ||||
|       component-test-batches: ${{ steps.determine.outputs.component-test-batches }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
| @@ -203,10 +205,17 @@ jobs: | ||||
|           # Extract individual fields | ||||
|           echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT | ||||
|           echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT | ||||
|           echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT | ||||
|           echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT | ||||
|           echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT | ||||
|           echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT | ||||
|           echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT | ||||
|           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT | ||||
|           echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT | ||||
|           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT | ||||
|           echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   integration-tests: | ||||
|     name: Run integration tests | ||||
| @@ -244,7 +253,34 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||
|  | ||||
|   clang-tidy: | ||||
|   cpp-unit-tests: | ||||
|     name: Run C++ unit tests | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|  | ||||
|       - name: Run cpp_unit_test.py | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then | ||||
|             script/cpp_unit_test.py --all | ||||
|           else | ||||
|             ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) | ||||
|             script/cpp_unit_test.py $ARGS | ||||
|           fi | ||||
|  | ||||
|   clang-tidy-single: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
| @@ -262,22 +298,6 @@ jobs: | ||||
|             name: Run script/clang-tidy for ESP8266 | ||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||
|             pio_cache_key: tidyesp8266 | ||||
|           - id: clang-tidy | ||||
|             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 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 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 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 IDF | ||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||
| @@ -358,70 +378,165 @@ jobs: | ||||
|         # yamllint disable-line rule:line-length | ||||
|         if: always() | ||||
|  | ||||
|   test-build-components: | ||||
|     name: Component test ${{ matrix.file }} | ||||
|   clang-tidy-nosplit: | ||||
|     name: Run script/clang-tidy for ESP32 Arduino | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100 | ||||
|     if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit' | ||||
|     env: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         with: | ||||
|           # Need history for HEAD~1 to work for checking changed files | ||||
|           fetch-depth: 2 | ||||
|  | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref == 'refs/heads/dev' | ||||
|         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref != 'refs/heads/dev' | ||||
|         uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|  | ||||
|       - name: Check if full clang-tidy scan needed | ||||
|         id: check_full_scan | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if python script/clang_tidy_hash.py --check; then | ||||
|             echo "full_scan=true" >> $GITHUB_OUTPUT | ||||
|             echo "reason=hash_changed" >> $GITHUB_OUTPUT | ||||
|           else | ||||
|             echo "full_scan=false" >> $GITHUB_OUTPUT | ||||
|             echo "reason=normal" >> $GITHUB_OUTPUT | ||||
|           fi | ||||
|  | ||||
|       - name: Run clang-tidy | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then | ||||
|             echo "Running FULL clang-tidy scan (hash changed)" | ||||
|             script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy | ||||
|           else | ||||
|             echo "Running clang-tidy on changed files only" | ||||
|             script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy | ||||
|           fi | ||||
|         env: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   clang-tidy-split: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: needs.determine-jobs.outputs.clang-tidy-mode == 'split' | ||||
|     env: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 2 | ||||
|       matrix: | ||||
|         file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }} | ||||
|     steps: | ||||
|       - name: Cache apt packages | ||||
|         uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3 | ||||
|         with: | ||||
|           packages: libsdl2-dev | ||||
|           version: 1.0 | ||||
|         include: | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 1/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 1 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 2/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 2 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 3/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 3 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 Arduino 4/4 | ||||
|             options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 | ||||
|  | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         with: | ||||
|           # Need history for HEAD~1 to work for checking changed files | ||||
|           fetch-depth: 2 | ||||
|  | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Validate config for ${{ matrix.file }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           python3 script/test_build_components.py -e config -c ${{ matrix.file }} | ||||
|       - name: Compile config for ${{ matrix.file }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           python3 script/test_build_components.py -e compile -c ${{ matrix.file }} | ||||
|  | ||||
|   test-build-components-splitter: | ||||
|     name: Split components for intelligent grouping (40 weighted per batch) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 | ||||
|     outputs: | ||||
|       matrix: ${{ steps.split.outputs.components }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|       - name: Cache platformio | ||||
|         if: github.ref == 'refs/heads/dev' | ||||
|         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Split components intelligently based on bus configurations | ||||
|         id: split | ||||
|           path: ~/.platformio | ||||
|           key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref != 'refs/heads/dev' | ||||
|         uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|  | ||||
|       - name: Check if full clang-tidy scan needed | ||||
|         id: check_full_scan | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if python script/clang_tidy_hash.py --check; then | ||||
|             echo "full_scan=true" >> $GITHUB_OUTPUT | ||||
|             echo "reason=hash_changed" >> $GITHUB_OUTPUT | ||||
|           else | ||||
|             echo "full_scan=false" >> $GITHUB_OUTPUT | ||||
|             echo "reason=normal" >> $GITHUB_OUTPUT | ||||
|           fi | ||||
|  | ||||
|           # Use intelligent splitter that groups components with same bus configs | ||||
|           components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' | ||||
|       - name: Run clang-tidy | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then | ||||
|             echo "Running FULL clang-tidy scan (hash changed)" | ||||
|             script/clang-tidy --all-headers --fix ${{ matrix.options }} | ||||
|           else | ||||
|             echo "Running clang-tidy on changed files only" | ||||
|             script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} | ||||
|           fi | ||||
|         env: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|           echo "Splitting components intelligently..." | ||||
|           output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github) | ||||
|  | ||||
|           echo "$output" >> $GITHUB_OUTPUT | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   test-build-components-split: | ||||
|     name: Test components batch (${{ matrix.components }}) | ||||
| @@ -429,13 +544,12 @@ jobs: | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|       - test-build-components-splitter | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }} | ||||
|       max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} | ||||
|       matrix: | ||||
|         components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} | ||||
|         components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} | ||||
|     steps: | ||||
|       - name: Show disk space | ||||
|         run: | | ||||
| @@ -461,41 +575,80 @@ jobs: | ||||
|       - name: Validate and compile components with intelligent grouping | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           # Use /mnt for build files (70GB available vs ~29GB on /) | ||||
|           # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there) | ||||
|           sudo mkdir -p /mnt/platformio | ||||
|           sudo chown $USER:$USER /mnt/platformio | ||||
|           mkdir -p ~/.platformio | ||||
|           sudo mount --bind /mnt/platformio ~/.platformio | ||||
|  | ||||
|           # Bind mount test build directory to /mnt | ||||
|           sudo mkdir -p /mnt/test_build_components_build | ||||
|           sudo chown $USER:$USER /mnt/test_build_components_build | ||||
|           mkdir -p tests/test_build_components/build | ||||
|           sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build | ||||
|           # Check if /mnt has more free space than / before bind mounting | ||||
|           # Extract available space in KB for comparison | ||||
|           root_avail=$(df -k / | awk 'NR==2 {print $4}') | ||||
|           mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}') | ||||
|  | ||||
|           echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB" | ||||
|  | ||||
|           # Only use /mnt if it has more space than / | ||||
|           if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then | ||||
|             echo "Using /mnt for build files (more space available)" | ||||
|             # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there) | ||||
|             sudo mkdir -p /mnt/platformio | ||||
|             sudo chown $USER:$USER /mnt/platformio | ||||
|             mkdir -p ~/.platformio | ||||
|             sudo mount --bind /mnt/platformio ~/.platformio | ||||
|  | ||||
|             # Bind mount test build directory to /mnt | ||||
|             sudo mkdir -p /mnt/test_build_components_build | ||||
|             sudo chown $USER:$USER /mnt/test_build_components_build | ||||
|             mkdir -p tests/test_build_components/build | ||||
|             sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build | ||||
|           else | ||||
|             echo "Using / for build files (more space available than /mnt or /mnt unavailable)" | ||||
|           fi | ||||
|  | ||||
|           # Convert space-separated components to comma-separated for Python script | ||||
|           components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',') | ||||
|  | ||||
|           echo "Testing components: $components_csv" | ||||
|           # Only isolate directly changed components when targeting dev branch | ||||
|           # For beta/release branches, group everything for faster CI | ||||
|           # | ||||
|           # WHY ISOLATE DIRECTLY CHANGED COMPONENTS? | ||||
|           # - Isolated tests run WITHOUT --testing-mode, enabling full validation | ||||
|           # - This catches pin conflicts and other issues in directly changed code | ||||
|           # - Grouped tests use --testing-mode to allow config merging (disables some checks) | ||||
|           # - Dependencies are safe to group since they weren't modified in this PR | ||||
|           if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then | ||||
|             directly_changed_csv="" | ||||
|             echo "Testing components: $components_csv" | ||||
|             echo "Target branch: ${{ github.base_ref }} - grouping all components" | ||||
|           else | ||||
|             directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")') | ||||
|             echo "Testing components: $components_csv" | ||||
|             echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv" | ||||
|           fi | ||||
|           echo "" | ||||
|  | ||||
|           # Run config validation with grouping | ||||
|           python3 script/test_build_components.py -e config -c "$components_csv" -f | ||||
|           # Show disk space before validation (after bind mounts setup) | ||||
|           echo "Disk space before config validation:" | ||||
|           df -h | ||||
|           echo "" | ||||
|  | ||||
|           # Run config validation with grouping and isolation | ||||
|           python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv" | ||||
|  | ||||
|           echo "" | ||||
|           echo "Config validation passed! Starting compilation..." | ||||
|           echo "" | ||||
|  | ||||
|           # Run compilation with grouping | ||||
|           python3 script/test_build_components.py -e compile -c "$components_csv" -f | ||||
|           # Show disk space before compilation | ||||
|           echo "Disk space before compilation:" | ||||
|           df -h | ||||
|           echo "" | ||||
|  | ||||
|           # Run compilation with grouping and isolation | ||||
|           python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv" | ||||
|  | ||||
|   pre-commit-ci-lite: | ||||
|     name: pre-commit.ci lite | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' | ||||
|     if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
| @@ -510,6 +663,271 @@ jobs: | ||||
|       - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 | ||||
|         if: always() | ||||
|  | ||||
|   memory-impact-target-branch: | ||||
|     name: Build target branch for memory impact | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' | ||||
|     outputs: | ||||
|       ram_usage: ${{ steps.extract.outputs.ram_usage }} | ||||
|       flash_usage: ${{ steps.extract.outputs.flash_usage }} | ||||
|       cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} | ||||
|       skip: ${{ steps.check-script.outputs.skip }} | ||||
|     steps: | ||||
|       - name: Check out target branch | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         with: | ||||
|           ref: ${{ github.base_ref }} | ||||
|  | ||||
|       # Check if memory impact extraction script exists on target branch | ||||
|       # If not, skip the analysis (this handles older branches that don't have the feature) | ||||
|       - name: Check for memory impact script | ||||
|         id: check-script | ||||
|         run: | | ||||
|           if [ -f "script/ci_memory_impact_extract.py" ]; then | ||||
|             echo "skip=false" >> $GITHUB_OUTPUT | ||||
|           else | ||||
|             echo "skip=true" >> $GITHUB_OUTPUT | ||||
|             echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" | ||||
|           fi | ||||
|  | ||||
|       # All remaining steps only run if script exists | ||||
|       - name: Generate cache key | ||||
|         id: cache-key | ||||
|         if: steps.check-script.outputs.skip != 'true' | ||||
|         run: | | ||||
|           # Get the commit SHA of the target branch | ||||
|           target_sha=$(git rev-parse HEAD) | ||||
|  | ||||
|           # Hash the build infrastructure files (all files that affect build/analysis) | ||||
|           infra_hash=$(cat \ | ||||
|             script/test_build_components.py \ | ||||
|             script/ci_memory_impact_extract.py \ | ||||
|             script/analyze_component_buses.py \ | ||||
|             script/merge_component_configs.py \ | ||||
|             script/ci_helpers.py \ | ||||
|             .github/workflows/ci.yml \ | ||||
|             | sha256sum | cut -d' ' -f1) | ||||
|  | ||||
|           # Get platform and components from job inputs | ||||
|           platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" | ||||
|           components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' | ||||
|           components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1) | ||||
|  | ||||
|           # Combine into cache key | ||||
|           cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}" | ||||
|           echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT | ||||
|           echo "Cache key: ${cache_key}" | ||||
|  | ||||
|       - name: Restore cached memory analysis | ||||
|         id: cache-memory-analysis | ||||
|         if: steps.check-script.outputs.skip != 'true' | ||||
|         uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: memory-analysis-target.json | ||||
|           key: ${{ steps.cache-key.outputs.cache-key }} | ||||
|  | ||||
|       - name: Cache status | ||||
|         if: steps.check-script.outputs.skip != 'true' | ||||
|         run: | | ||||
|           if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then | ||||
|             echo "✓ Cache hit! Using cached memory analysis results." | ||||
|             echo "  Skipping build step to save time." | ||||
|           else | ||||
|             echo "✗ Cache miss. Will build and analyze memory usage." | ||||
|           fi | ||||
|  | ||||
|       - name: Restore Python | ||||
|         if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' | ||||
|         uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} | ||||
|  | ||||
|       - name: Build, compile, and analyze memory | ||||
|         if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' | ||||
|         id: build | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' | ||||
|           platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" | ||||
|  | ||||
|           echo "Building with test_build_components.py for $platform with components:" | ||||
|           echo "$components" | jq -r '.[]' | sed 's/^/  - /' | ||||
|  | ||||
|           # Use test_build_components.py which handles grouping automatically | ||||
|           # Pass components as comma-separated list | ||||
|           component_list=$(echo "$components" | jq -r 'join(",")') | ||||
|  | ||||
|           echo "Compiling with test_build_components.py..." | ||||
|  | ||||
|           # Run build and extract memory with auto-detection of build directory for detailed analysis | ||||
|           # Use tee to show output in CI while also piping to extraction script | ||||
|           python script/test_build_components.py \ | ||||
|             -e compile \ | ||||
|             -c "$component_list" \ | ||||
|             -t "$platform" 2>&1 | \ | ||||
|             tee /dev/stderr | \ | ||||
|             python script/ci_memory_impact_extract.py \ | ||||
|               --output-env \ | ||||
|               --output-json memory-analysis-target.json | ||||
|  | ||||
|           # Add metadata to JSON before caching | ||||
|           python script/ci_add_metadata_to_json.py \ | ||||
|             --json-file memory-analysis-target.json \ | ||||
|             --components "$components" \ | ||||
|             --platform "$platform" | ||||
|  | ||||
|       - name: Save memory analysis to cache | ||||
|         if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' | ||||
|         uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: memory-analysis-target.json | ||||
|           key: ${{ steps.cache-key.outputs.cache-key }} | ||||
|  | ||||
|       - name: Extract memory usage for outputs | ||||
|         id: extract | ||||
|         if: steps.check-script.outputs.skip != 'true' | ||||
|         run: | | ||||
|           if [ -f memory-analysis-target.json ]; then | ||||
|             ram=$(jq -r '.ram_bytes' memory-analysis-target.json) | ||||
|             flash=$(jq -r '.flash_bytes' memory-analysis-target.json) | ||||
|             echo "ram_usage=${ram}" >> $GITHUB_OUTPUT | ||||
|             echo "flash_usage=${flash}" >> $GITHUB_OUTPUT | ||||
|             echo "RAM: ${ram} bytes, Flash: ${flash} bytes" | ||||
|           else | ||||
|             echo "Error: memory-analysis-target.json not found" | ||||
|             exit 1 | ||||
|           fi | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: memory-analysis-target.json | ||||
|           if-no-files-found: warn | ||||
|           retention-days: 1 | ||||
|  | ||||
|   memory-impact-pr-branch: | ||||
|     name: Build PR branch for memory impact | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' | ||||
|     outputs: | ||||
|       ram_usage: ${{ steps.extract.outputs.ram_usage }} | ||||
|       flash_usage: ${{ steps.extract.outputs.flash_usage }} | ||||
|     steps: | ||||
|       - name: Check out PR branch | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} | ||||
|       - name: Build, compile, and analyze memory | ||||
|         id: extract | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' | ||||
|           platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" | ||||
|  | ||||
|           echo "Building with test_build_components.py for $platform with components:" | ||||
|           echo "$components" | jq -r '.[]' | sed 's/^/  - /' | ||||
|  | ||||
|           # Use test_build_components.py which handles grouping automatically | ||||
|           # Pass components as comma-separated list | ||||
|           component_list=$(echo "$components" | jq -r 'join(",")') | ||||
|  | ||||
|           echo "Compiling with test_build_components.py..." | ||||
|  | ||||
|           # Run build and extract memory with auto-detection of build directory for detailed analysis | ||||
|           # Use tee to show output in CI while also piping to extraction script | ||||
|           python script/test_build_components.py \ | ||||
|             -e compile \ | ||||
|             -c "$component_list" \ | ||||
|             -t "$platform" 2>&1 | \ | ||||
|             tee /dev/stderr | \ | ||||
|             python script/ci_memory_impact_extract.py \ | ||||
|               --output-env \ | ||||
|               --output-json memory-analysis-pr.json | ||||
|  | ||||
|           # Add metadata to JSON (components and platform are in shell variables above) | ||||
|           python script/ci_add_metadata_to_json.py \ | ||||
|             --json-file memory-analysis-pr.json \ | ||||
|             --components "$components" \ | ||||
|             --platform "$platform" | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: memory-analysis-pr.json | ||||
|           if-no-files-found: warn | ||||
|           retention-days: 1 | ||||
|  | ||||
|   memory-impact-comment: | ||||
|     name: Comment memory impact | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|       - memory-impact-target-branch | ||||
|       - memory-impact-pr-branch | ||||
|     if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true' | ||||
|     permissions: | ||||
|       contents: read | ||||
|       pull-requests: write | ||||
|     env: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     steps: | ||||
|       - name: Check out code | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Download target analysis JSON | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: ./memory-analysis | ||||
|         continue-on-error: true | ||||
|       - name: Download PR analysis JSON | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: ./memory-analysis | ||||
|         continue-on-error: true | ||||
|       - name: Post or update PR comment | ||||
|         env: | ||||
|           PR_NUMBER: ${{ github.event.pull_request.number }} | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|  | ||||
|           # Pass JSON file paths directly to Python script | ||||
|           # All data is extracted from JSON files for security | ||||
|           python script/ci_memory_impact_comment.py \ | ||||
|             --pr-number "$PR_NUMBER" \ | ||||
|             --target-json ./memory-analysis/memory-analysis-target.json \ | ||||
|             --pr-json ./memory-analysis/memory-analysis-pr.json | ||||
|  | ||||
|   ci-status: | ||||
|     name: CI Status | ||||
|     runs-on: ubuntu-24.04 | ||||
| @@ -519,12 +937,15 @@ jobs: | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - integration-tests | ||||
|       - clang-tidy | ||||
|       - clang-tidy-single | ||||
|       - clang-tidy-nosplit | ||||
|       - clang-tidy-split | ||||
|       - determine-jobs | ||||
|       - test-build-components | ||||
|       - test-build-components-splitter | ||||
|       - test-build-components-split | ||||
|       - pre-commit-ci-lite | ||||
|       - memory-impact-target-branch | ||||
|       - memory-impact-pr-branch | ||||
|       - memory-impact-comment | ||||
|     if: always() | ||||
|     steps: | ||||
|       - name: Success | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							| @@ -58,7 +58,7 @@ jobs: | ||||
|  | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 | ||||
|         uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|           build-mode: ${{ matrix.build-mode }} | ||||
| @@ -86,6 +86,6 @@ jobs: | ||||
|           exit 1 | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 | ||||
|         uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 | ||||
|         with: | ||||
|           category: "/language:${{matrix.language}}" | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -138,7 +138,7 @@ jobs: | ||||
|       #     version: ${{ needs.init.outputs.tag }} | ||||
|  | ||||
|       - name: Upload digests | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: digests-${{ matrix.platform.arch }} | ||||
|           path: /tmp/digests | ||||
| @@ -171,7 +171,7 @@ jobs: | ||||
|       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           pattern: digests-* | ||||
|           path: /tmp/digests | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|         with: | ||||
|           debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch | ||||
|           remove-stale-when-updated: true | ||||
|           operations-per-run: 150 | ||||
|           operations-per-run: 400 | ||||
|  | ||||
|           # The 90 day stale policy for PRs | ||||
|           # - PRs | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,6 +14,7 @@ jobs: | ||||
|         label: | ||||
|           - needs-docs | ||||
|           - merge-after-release | ||||
|           - chained-pr | ||||
|     steps: | ||||
|       - name: Check for ${{ matrix.label }} label | ||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ ci: | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.14.0 | ||||
|     rev: v0.14.2 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -62,6 +62,7 @@ esphome/components/bedjet/fan/* @jhansche | ||||
| esphome/components/bedjet/sensor/* @javawizard @jhansche | ||||
| esphome/components/beken_spi_led_strip/* @Mat931 | ||||
| esphome/components/bh1750/* @OttoWinter | ||||
| esphome/components/bh1900nux/* @B48D81EFCC | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bk72xx/* @kuba2k2 | ||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||
| @@ -69,6 +70,7 @@ esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @dan-s-github @tobias- | ||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | ||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | ||||
| esphome/components/ble_nus/* @tomaszduda23 | ||||
| esphome/components/bluetooth_proxy/* @bdraco @jesserockz | ||||
| esphome/components/bme280_base/* @esphome/core | ||||
| esphome/components/bme280_spi/* @apbodrov | ||||
| @@ -159,6 +161,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/esp_ldo/* @clydebarrow | ||||
| esphome/components/espnow/* @jesserockz | ||||
| esphome/components/espnow/packet_transport/* @EasilyBoredEngineer | ||||
| esphome/components/ethernet_info/* @gtjadsonsantos | ||||
| esphome/components/event/* @nohat | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| @@ -198,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/hbridge/switch/* @dwmw2 | ||||
| esphome/components/hdc2010/* @optimusprimespace @ssieb | ||||
| esphome/components/he60r/* @clydebarrow | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
|   | ||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							| @@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome | ||||
| # could be handy for archiving the generated documentation or if some version | ||||
| # control system is used. | ||||
|  | ||||
| PROJECT_NUMBER         = 2025.10.2 | ||||
| PROJECT_NUMBER         = 2025.11.0-dev | ||||
|  | ||||
| # Using the PROJECT_BRIEF tag one can provide an optional one line description | ||||
| # for a project that appears at the top of each page and should give viewer a | ||||
|   | ||||
| @@ -62,6 +62,40 @@ from esphome.util import ( | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # Special non-component keys that appear in configs | ||||
| _NON_COMPONENT_KEYS = frozenset( | ||||
|     { | ||||
|         CONF_ESPHOME, | ||||
|         "substitutions", | ||||
|         "packages", | ||||
|         "globals", | ||||
|         "external_components", | ||||
|         "<<", | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| def detect_external_components(config: ConfigType) -> set[str]: | ||||
|     """Detect external/custom components in the configuration. | ||||
|  | ||||
|     External components are those that appear in the config but are not | ||||
|     part of ESPHome's built-in components and are not special config keys. | ||||
|  | ||||
|     Args: | ||||
|         config: The ESPHome configuration dictionary | ||||
|  | ||||
|     Returns: | ||||
|         A set of external component names | ||||
|     """ | ||||
|     from esphome.analyze_memory.helpers import get_esphome_components | ||||
|  | ||||
|     builtin_components = get_esphome_components() | ||||
|     return { | ||||
|         key | ||||
|         for key in config | ||||
|         if key not in builtin_components and key not in _NON_COMPONENT_KEYS | ||||
|     } | ||||
|  | ||||
|  | ||||
| class ArgsProtocol(Protocol): | ||||
|     device: list[str] | None | ||||
| @@ -173,14 +207,14 @@ def choose_upload_log_host( | ||||
|                     if has_mqtt_logging(): | ||||
|                         resolved.append("MQTT") | ||||
|  | ||||
|                     if has_api() and has_non_ip_address(): | ||||
|                     if has_api() and has_non_ip_address() and has_resolvable_address(): | ||||
|                         resolved.extend(_resolve_with_cache(CORE.address, purpose)) | ||||
|  | ||||
|                 elif purpose == Purpose.UPLOADING: | ||||
|                     if has_ota() and has_mqtt_ip_lookup(): | ||||
|                         resolved.append("MQTTIP") | ||||
|  | ||||
|                     if has_ota() and has_non_ip_address(): | ||||
|                     if has_ota() and has_non_ip_address() and has_resolvable_address(): | ||||
|                         resolved.extend(_resolve_with_cache(CORE.address, purpose)) | ||||
|             else: | ||||
|                 resolved.append(device) | ||||
| @@ -284,7 +318,17 @@ def has_resolvable_address() -> bool: | ||||
|     """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" | ||||
|     # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable | ||||
|     # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver | ||||
|     return CORE.address is not None | ||||
|     if CORE.address is None: | ||||
|         return False | ||||
|  | ||||
|     if has_ip_address(): | ||||
|         return True | ||||
|  | ||||
|     if has_mdns(): | ||||
|         return True | ||||
|  | ||||
|     # .local mDNS hostnames are only resolvable if mDNS is enabled | ||||
|     return not CORE.address.endswith(".local") | ||||
|  | ||||
|  | ||||
| def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): | ||||
| @@ -468,7 +512,9 @@ def write_cpp_file() -> int: | ||||
| def compile_program(args: ArgsProtocol, config: ConfigType) -> int: | ||||
|     from esphome import platformio_api | ||||
|  | ||||
|     _LOGGER.info("Compiling app...") | ||||
|     # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py | ||||
|     # If you change this format, update the regex in that script as well | ||||
|     _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) | ||||
|     rc = platformio_api.run_compile(config, CORE.verbose) | ||||
|     if rc != 0: | ||||
|         return rc | ||||
| @@ -890,6 +936,54 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: | ||||
|     """Analyze memory usage by component. | ||||
|  | ||||
|     This command compiles the configuration and performs memory analysis. | ||||
|     Compilation is fast if sources haven't changed (just relinking). | ||||
|     """ | ||||
|     from esphome import platformio_api | ||||
|     from esphome.analyze_memory.cli import MemoryAnalyzerCLI | ||||
|  | ||||
|     # Always compile to ensure fresh data (fast if no changes - just relinks) | ||||
|     exit_code = write_cpp(config) | ||||
|     if exit_code != 0: | ||||
|         return exit_code | ||||
|     exit_code = compile_program(args, config) | ||||
|     if exit_code != 0: | ||||
|         return exit_code | ||||
|     _LOGGER.info("Successfully compiled program.") | ||||
|  | ||||
|     # Get idedata for analysis | ||||
|     idedata = platformio_api.get_idedata(config) | ||||
|     if idedata is None: | ||||
|         _LOGGER.error("Failed to get IDE data for memory analysis") | ||||
|         return 1 | ||||
|  | ||||
|     firmware_elf = Path(idedata.firmware_elf_path) | ||||
|  | ||||
|     # Extract external components from config | ||||
|     external_components = detect_external_components(config) | ||||
|     _LOGGER.debug("Detected external components: %s", external_components) | ||||
|  | ||||
|     # Perform memory analysis | ||||
|     _LOGGER.info("Analyzing memory usage...") | ||||
|     analyzer = MemoryAnalyzerCLI( | ||||
|         str(firmware_elf), | ||||
|         idedata.objdump_path, | ||||
|         idedata.readelf_path, | ||||
|         external_components, | ||||
|     ) | ||||
|     analyzer.analyze() | ||||
|  | ||||
|     # Generate and display report | ||||
|     report = analyzer.generate_report() | ||||
|     print() | ||||
|     print(report) | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     new_name = args.name | ||||
|     for c in new_name: | ||||
| @@ -1005,6 +1099,7 @@ POST_CONFIG_ACTIONS = { | ||||
|     "idedata": command_idedata, | ||||
|     "rename": command_rename, | ||||
|     "discover": command_discover, | ||||
|     "analyze-memory": command_analyze_memory, | ||||
| } | ||||
|  | ||||
| SIMPLE_CONFIG_ACTIONS = [ | ||||
| @@ -1290,6 +1385,14 @@ def parse_args(argv): | ||||
|     ) | ||||
|     parser_rename.add_argument("name", help="The new name for the device.", type=str) | ||||
|  | ||||
|     parser_analyze_memory = subparsers.add_parser( | ||||
|         "analyze-memory", | ||||
|         help="Analyze memory usage by component.", | ||||
|     ) | ||||
|     parser_analyze_memory.add_argument( | ||||
|         "configuration", help="Your YAML configuration file(s).", nargs="+" | ||||
|     ) | ||||
|  | ||||
|     # Keep backward compatibility with the old command line format of | ||||
|     # esphome <config> <command>. | ||||
|     # | ||||
|   | ||||
							
								
								
									
										502
									
								
								esphome/analyze_memory/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										502
									
								
								esphome/analyze_memory/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,502 @@ | ||||
| """Memory usage analyzer for ESPHome compiled binaries.""" | ||||
|  | ||||
| from collections import defaultdict | ||||
| from dataclasses import dataclass, field | ||||
| import logging | ||||
| from pathlib import Path | ||||
| import re | ||||
| import subprocess | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from .const import ( | ||||
|     CORE_SUBCATEGORY_PATTERNS, | ||||
|     DEMANGLED_PATTERNS, | ||||
|     ESPHOME_COMPONENT_PATTERN, | ||||
|     SECTION_TO_ATTR, | ||||
|     SYMBOL_PATTERNS, | ||||
| ) | ||||
| from .helpers import ( | ||||
|     get_component_class_patterns, | ||||
|     get_esphome_components, | ||||
|     map_section_name, | ||||
|     parse_symbol_line, | ||||
| ) | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from esphome.platformio_api import IDEData | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # GCC global constructor/destructor prefix annotations | ||||
| _GCC_PREFIX_ANNOTATIONS = { | ||||
|     "_GLOBAL__sub_I_": "global constructor for", | ||||
|     "_GLOBAL__sub_D_": "global destructor for", | ||||
| } | ||||
|  | ||||
| # GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) | ||||
| _GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") | ||||
|  | ||||
| # C++ runtime patterns for categorization | ||||
| _CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) | ||||
|  | ||||
| # libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.) | ||||
| _LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"]) | ||||
|  | ||||
| # Regex pattern for parsing readelf section headers | ||||
| # Format: [ #] name type addr off size | ||||
| _READELF_SECTION_PATTERN = re.compile( | ||||
|     r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" | ||||
| ) | ||||
|  | ||||
| # Component category prefixes | ||||
| _COMPONENT_PREFIX_ESPHOME = "[esphome]" | ||||
| _COMPONENT_PREFIX_EXTERNAL = "[external]" | ||||
| _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" | ||||
| _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" | ||||
|  | ||||
| # C++ namespace prefixes | ||||
| _NAMESPACE_ESPHOME = "esphome::" | ||||
| _NAMESPACE_STD = "std::" | ||||
|  | ||||
| # Type alias for symbol information: (symbol_name, size, component) | ||||
| SymbolInfoType = tuple[str, int, str] | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class MemorySection: | ||||
|     """Represents a memory section with its symbols.""" | ||||
|  | ||||
|     name: str | ||||
|     symbols: list[SymbolInfoType] = field(default_factory=list) | ||||
|     total_size: int = 0 | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class ComponentMemory: | ||||
|     """Tracks memory usage for a component.""" | ||||
|  | ||||
|     name: str | ||||
|     text_size: int = 0  # Code in flash | ||||
|     rodata_size: int = 0  # Read-only data in flash | ||||
|     data_size: int = 0  # Initialized data (flash + ram) | ||||
|     bss_size: int = 0  # Uninitialized data (ram only) | ||||
|     symbol_count: int = 0 | ||||
|  | ||||
|     @property | ||||
|     def flash_total(self) -> int: | ||||
|         """Total flash usage (text + rodata + data).""" | ||||
|         return self.text_size + self.rodata_size + self.data_size | ||||
|  | ||||
|     @property | ||||
|     def ram_total(self) -> int: | ||||
|         """Total RAM usage (data + bss).""" | ||||
|         return self.data_size + self.bss_size | ||||
|  | ||||
|  | ||||
| class MemoryAnalyzer: | ||||
|     """Analyzes memory usage from ELF files.""" | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         elf_path: str, | ||||
|         objdump_path: str | None = None, | ||||
|         readelf_path: str | None = None, | ||||
|         external_components: set[str] | None = None, | ||||
|         idedata: "IDEData | None" = None, | ||||
|     ) -> None: | ||||
|         """Initialize memory analyzer. | ||||
|  | ||||
|         Args: | ||||
|             elf_path: Path to ELF file to analyze | ||||
|             objdump_path: Path to objdump binary (auto-detected from idedata if not provided) | ||||
|             readelf_path: Path to readelf binary (auto-detected from idedata if not provided) | ||||
|             external_components: Set of external component names | ||||
|             idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths | ||||
|         """ | ||||
|         self.elf_path = Path(elf_path) | ||||
|         if not self.elf_path.exists(): | ||||
|             raise FileNotFoundError(f"ELF file not found: {elf_path}") | ||||
|  | ||||
|         # Auto-detect toolchain paths from idedata if not provided | ||||
|         if idedata is not None and (objdump_path is None or readelf_path is None): | ||||
|             objdump_path = objdump_path or idedata.objdump_path | ||||
|             readelf_path = readelf_path or idedata.readelf_path | ||||
|             _LOGGER.debug("Using toolchain paths from PlatformIO idedata") | ||||
|  | ||||
|         self.objdump_path = objdump_path or "objdump" | ||||
|         self.readelf_path = readelf_path or "readelf" | ||||
|         self.external_components = external_components or set() | ||||
|  | ||||
|         self.sections: dict[str, MemorySection] = {} | ||||
|         self.components: dict[str, ComponentMemory] = defaultdict( | ||||
|             lambda: ComponentMemory("") | ||||
|         ) | ||||
|         self._demangle_cache: dict[str, str] = {} | ||||
|         self._uncategorized_symbols: list[tuple[str, str, int]] = [] | ||||
|         self._esphome_core_symbols: list[ | ||||
|             tuple[str, str, int] | ||||
|         ] = []  # Track core symbols | ||||
|         self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( | ||||
|             list | ||||
|         )  # Track symbols for all components | ||||
|  | ||||
|     def analyze(self) -> dict[str, ComponentMemory]: | ||||
|         """Analyze the ELF file and return component memory usage.""" | ||||
|         self._parse_sections() | ||||
|         self._parse_symbols() | ||||
|         self._categorize_symbols() | ||||
|         return dict(self.components) | ||||
|  | ||||
|     def _parse_sections(self) -> None: | ||||
|         """Parse section headers from ELF file.""" | ||||
|         result = subprocess.run( | ||||
|             [self.readelf_path, "-S", str(self.elf_path)], | ||||
|             capture_output=True, | ||||
|             text=True, | ||||
|             check=True, | ||||
|         ) | ||||
|  | ||||
|         # Parse section headers | ||||
|         for line in result.stdout.splitlines(): | ||||
|             # Look for section entries | ||||
|             if not (match := _READELF_SECTION_PATTERN.match(line)): | ||||
|                 continue | ||||
|  | ||||
|             section_name = match.group(1) | ||||
|             size_hex = match.group(2) | ||||
|             size = int(size_hex, 16) | ||||
|  | ||||
|             # Map to standard section name | ||||
|             mapped_section = map_section_name(section_name) | ||||
|             if not mapped_section: | ||||
|                 continue | ||||
|  | ||||
|             if mapped_section not in self.sections: | ||||
|                 self.sections[mapped_section] = MemorySection(mapped_section) | ||||
|             self.sections[mapped_section].total_size += size | ||||
|  | ||||
|     def _parse_symbols(self) -> None: | ||||
|         """Parse symbols from ELF file.""" | ||||
|         result = subprocess.run( | ||||
|             [self.objdump_path, "-t", str(self.elf_path)], | ||||
|             capture_output=True, | ||||
|             text=True, | ||||
|             check=True, | ||||
|         ) | ||||
|  | ||||
|         # Track seen addresses to avoid duplicates | ||||
|         seen_addresses: set[str] = set() | ||||
|  | ||||
|         for line in result.stdout.splitlines(): | ||||
|             if not (symbol_info := parse_symbol_line(line)): | ||||
|                 continue | ||||
|  | ||||
|             section, name, size, address = symbol_info | ||||
|  | ||||
|             # Skip duplicate symbols at the same address (e.g., C1/C2 constructors) | ||||
|             if address in seen_addresses or section not in self.sections: | ||||
|                 continue | ||||
|  | ||||
|             self.sections[section].symbols.append((name, size, "")) | ||||
|             seen_addresses.add(address) | ||||
|  | ||||
|     def _categorize_symbols(self) -> None: | ||||
|         """Categorize symbols by component.""" | ||||
|         # First, collect all unique symbol names for batch demangling | ||||
|         all_symbols = { | ||||
|             symbol_name | ||||
|             for section in self.sections.values() | ||||
|             for symbol_name, _, _ in section.symbols | ||||
|         } | ||||
|  | ||||
|         # Batch demangle all symbols at once | ||||
|         self._batch_demangle_symbols(list(all_symbols)) | ||||
|  | ||||
|         # Now categorize with cached demangled names | ||||
|         for section_name, section in self.sections.items(): | ||||
|             for symbol_name, size, _ in section.symbols: | ||||
|                 component = self._identify_component(symbol_name) | ||||
|  | ||||
|                 if component not in self.components: | ||||
|                     self.components[component] = ComponentMemory(component) | ||||
|  | ||||
|                 comp_mem = self.components[component] | ||||
|                 comp_mem.symbol_count += 1 | ||||
|  | ||||
|                 # Update the appropriate size attribute based on section | ||||
|                 if attr_name := SECTION_TO_ATTR.get(section_name): | ||||
|                     setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) | ||||
|  | ||||
|                 # Track uncategorized symbols | ||||
|                 if component == "other" and size > 0: | ||||
|                     demangled = self._demangle_symbol(symbol_name) | ||||
|                     self._uncategorized_symbols.append((symbol_name, demangled, size)) | ||||
|  | ||||
|                 # Track ESPHome core symbols for detailed analysis | ||||
|                 if component == _COMPONENT_CORE and size > 0: | ||||
|                     demangled = self._demangle_symbol(symbol_name) | ||||
|                     self._esphome_core_symbols.append((symbol_name, demangled, size)) | ||||
|  | ||||
|                 # Track all component symbols for detailed analysis | ||||
|                 if size > 0: | ||||
|                     demangled = self._demangle_symbol(symbol_name) | ||||
|                     self._component_symbols[component].append( | ||||
|                         (symbol_name, demangled, size) | ||||
|                     ) | ||||
|  | ||||
|     def _identify_component(self, symbol_name: str) -> str: | ||||
|         """Identify which component a symbol belongs to.""" | ||||
|         # Demangle C++ names if needed | ||||
|         demangled = self._demangle_symbol(symbol_name) | ||||
|  | ||||
|         # Check for special component classes first (before namespace pattern) | ||||
|         # This handles cases like esphome::ESPHomeOTAComponent which should map to ota | ||||
|         if _NAMESPACE_ESPHOME in demangled: | ||||
|             # Check for special component classes that include component name in the class | ||||
|             # For example: esphome::ESPHomeOTAComponent -> ota component | ||||
|             for component_name in get_esphome_components(): | ||||
|                 patterns = get_component_class_patterns(component_name) | ||||
|                 if any(pattern in demangled for pattern in patterns): | ||||
|                     return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" | ||||
|  | ||||
|         # Check for ESPHome component namespaces | ||||
|         match = ESPHOME_COMPONENT_PATTERN.search(demangled) | ||||
|         if match: | ||||
|             component_name = match.group(1) | ||||
|             # Strip trailing underscore if present (e.g., switch_ -> switch) | ||||
|             component_name = component_name.rstrip("_") | ||||
|  | ||||
|             # Check if this is an actual component in the components directory | ||||
|             if component_name in get_esphome_components(): | ||||
|                 return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" | ||||
|             # Check if this is a known external component from the config | ||||
|             if component_name in self.external_components: | ||||
|                 return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" | ||||
|             # Everything else in esphome:: namespace is core | ||||
|             return _COMPONENT_CORE | ||||
|  | ||||
|         # Check for esphome core namespace (no component namespace) | ||||
|         if _NAMESPACE_ESPHOME in demangled: | ||||
|             # If no component match found, it's core | ||||
|             return _COMPONENT_CORE | ||||
|  | ||||
|         # Check against symbol patterns | ||||
|         for component, patterns in SYMBOL_PATTERNS.items(): | ||||
|             if any(pattern in symbol_name for pattern in patterns): | ||||
|                 return component | ||||
|  | ||||
|         # Check against demangled patterns | ||||
|         for component, patterns in DEMANGLED_PATTERNS.items(): | ||||
|             if any(pattern in demangled for pattern in patterns): | ||||
|                 return component | ||||
|  | ||||
|         # Special cases that need more complex logic | ||||
|  | ||||
|         # Check if spi_flash vs spi_driver | ||||
|         if "spi_" in symbol_name or "SPI" in symbol_name: | ||||
|             return "spi_flash" if "spi_flash" in symbol_name else "spi_driver" | ||||
|  | ||||
|         # libc special printf variants | ||||
|         if ( | ||||
|             symbol_name.startswith("_") | ||||
|             and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "") | ||||
|             in _LIBC_PRINTF_SCANF_FAMILY | ||||
|         ): | ||||
|             return "libc" | ||||
|  | ||||
|         # Track uncategorized symbols for analysis | ||||
|         return "other" | ||||
|  | ||||
|     def _batch_demangle_symbols(self, symbols: list[str]) -> None: | ||||
|         """Batch demangle C++ symbol names for efficiency.""" | ||||
|         if not symbols: | ||||
|             return | ||||
|  | ||||
|         # Try to find the appropriate c++filt for the platform | ||||
|         cppfilt_cmd = "c++filt" | ||||
|  | ||||
|         _LOGGER.info("Demangling %d symbols", len(symbols)) | ||||
|         _LOGGER.debug("objdump_path = %s", self.objdump_path) | ||||
|  | ||||
|         # Check if we have a toolchain-specific c++filt | ||||
|         if self.objdump_path and self.objdump_path != "objdump": | ||||
|             # Replace objdump with c++filt in the path | ||||
|             potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") | ||||
|             _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt) | ||||
|             if Path(potential_cppfilt).exists(): | ||||
|                 cppfilt_cmd = potential_cppfilt | ||||
|                 _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd) | ||||
|             else: | ||||
|                 _LOGGER.info( | ||||
|                     "✗ Toolchain c++filt not found at %s, using system c++filt", | ||||
|                     potential_cppfilt, | ||||
|                 ) | ||||
|         else: | ||||
|             _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) | ||||
|  | ||||
|         # Strip GCC optimization suffixes and prefixes before demangling | ||||
|         # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt | ||||
|         # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked | ||||
|         symbols_stripped: list[str] = [] | ||||
|         symbols_prefixes: list[str] = []  # Track removed prefixes | ||||
|         for symbol in symbols: | ||||
|             # Remove GCC optimization markers | ||||
|             stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) | ||||
|  | ||||
|             # Handle GCC global constructor/initializer prefixes | ||||
|             # _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling | ||||
|             prefix = "" | ||||
|             for gcc_prefix in _GCC_PREFIX_ANNOTATIONS: | ||||
|                 if stripped.startswith(gcc_prefix): | ||||
|                     prefix = gcc_prefix | ||||
|                     stripped = stripped[len(prefix) :] | ||||
|                     break | ||||
|  | ||||
|             symbols_stripped.append(stripped) | ||||
|             symbols_prefixes.append(prefix) | ||||
|  | ||||
|         try: | ||||
|             # Send all symbols to c++filt at once | ||||
|             result = subprocess.run( | ||||
|                 [cppfilt_cmd], | ||||
|                 input="\n".join(symbols_stripped), | ||||
|                 capture_output=True, | ||||
|                 text=True, | ||||
|                 check=False, | ||||
|             ) | ||||
|         except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: | ||||
|             # On error, cache originals | ||||
|             _LOGGER.warning("Failed to batch demangle symbols: %s", e) | ||||
|             for symbol in symbols: | ||||
|                 self._demangle_cache[symbol] = symbol | ||||
|             return | ||||
|  | ||||
|         if result.returncode != 0: | ||||
|             _LOGGER.warning( | ||||
|                 "c++filt exited with code %d: %s", | ||||
|                 result.returncode, | ||||
|                 result.stderr[:200] if result.stderr else "(no error output)", | ||||
|             ) | ||||
|             # Cache originals on failure | ||||
|             for symbol in symbols: | ||||
|                 self._demangle_cache[symbol] = symbol | ||||
|             return | ||||
|  | ||||
|         # Process demangled output | ||||
|         self._process_demangled_output( | ||||
|             symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd | ||||
|         ) | ||||
|  | ||||
|     def _process_demangled_output( | ||||
|         self, | ||||
|         symbols: list[str], | ||||
|         symbols_stripped: list[str], | ||||
|         symbols_prefixes: list[str], | ||||
|         demangled_output: str, | ||||
|         cppfilt_cmd: str, | ||||
|     ) -> None: | ||||
|         """Process demangled symbol output and populate cache. | ||||
|  | ||||
|         Args: | ||||
|             symbols: Original symbol names | ||||
|             symbols_stripped: Stripped symbol names sent to c++filt | ||||
|             symbols_prefixes: Removed prefixes to restore | ||||
|             demangled_output: Output from c++filt | ||||
|             cppfilt_cmd: Path to c++filt command (for logging) | ||||
|         """ | ||||
|         demangled_lines = demangled_output.strip().split("\n") | ||||
|         failed_count = 0 | ||||
|  | ||||
|         for original, stripped, prefix, demangled in zip( | ||||
|             symbols, symbols_stripped, symbols_prefixes, demangled_lines | ||||
|         ): | ||||
|             # Add back any prefix that was removed | ||||
|             demangled = self._restore_symbol_prefix(prefix, stripped, demangled) | ||||
|  | ||||
|             # If we stripped a suffix, add it back to the demangled name for clarity | ||||
|             if original != stripped and not prefix: | ||||
|                 demangled = self._restore_symbol_suffix(original, demangled) | ||||
|  | ||||
|             self._demangle_cache[original] = demangled | ||||
|  | ||||
|             # Log symbols that failed to demangle (stayed the same as stripped version) | ||||
|             if stripped == demangled and stripped.startswith("_Z"): | ||||
|                 failed_count += 1 | ||||
|                 if failed_count <= 5:  # Only log first 5 failures | ||||
|                     _LOGGER.warning("Failed to demangle: %s", original) | ||||
|  | ||||
|         if failed_count == 0: | ||||
|             _LOGGER.info("Successfully demangled all %d symbols", len(symbols)) | ||||
|             return | ||||
|  | ||||
|         _LOGGER.warning( | ||||
|             "Failed to demangle %d/%d symbols using %s", | ||||
|             failed_count, | ||||
|             len(symbols), | ||||
|             cppfilt_cmd, | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: | ||||
|         """Restore prefix that was removed before demangling. | ||||
|  | ||||
|         Args: | ||||
|             prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") | ||||
|             stripped: Stripped symbol name | ||||
|             demangled: Demangled symbol name | ||||
|  | ||||
|         Returns: | ||||
|             Demangled name with prefix restored/annotated | ||||
|         """ | ||||
|         if not prefix: | ||||
|             return demangled | ||||
|  | ||||
|         # Successfully demangled - add descriptive prefix | ||||
|         if demangled != stripped and ( | ||||
|             annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix) | ||||
|         ): | ||||
|             return f"[{annotation}: {demangled}]" | ||||
|  | ||||
|         # Failed to demangle - restore original prefix | ||||
|         return prefix + demangled | ||||
|  | ||||
|     @staticmethod | ||||
|     def _restore_symbol_suffix(original: str, demangled: str) -> str: | ||||
|         """Restore GCC optimization suffix that was removed before demangling. | ||||
|  | ||||
|         Args: | ||||
|             original: Original symbol name with suffix | ||||
|             demangled: Demangled symbol name without suffix | ||||
|  | ||||
|         Returns: | ||||
|             Demangled name with suffix annotation | ||||
|         """ | ||||
|         if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): | ||||
|             return f"{demangled} [{suffix_match.group(1)}]" | ||||
|         return demangled | ||||
|  | ||||
|     def _demangle_symbol(self, symbol: str) -> str: | ||||
|         """Get demangled C++ symbol name from cache.""" | ||||
|         return self._demangle_cache.get(symbol, symbol) | ||||
|  | ||||
|     def _categorize_esphome_core_symbol(self, demangled: str) -> str: | ||||
|         """Categorize ESPHome core symbols into subcategories.""" | ||||
|         # Special patterns that need to be checked separately | ||||
|         if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS): | ||||
|             return "C++ Runtime (vtables/RTTI)" | ||||
|  | ||||
|         if demangled.startswith(_NAMESPACE_STD): | ||||
|             return "C++ STL" | ||||
|  | ||||
|         # Check against patterns from const.py | ||||
|         for category, patterns in CORE_SUBCATEGORY_PATTERNS.items(): | ||||
|             if any(pattern in demangled for pattern in patterns): | ||||
|                 return category | ||||
|  | ||||
|         return "Other Core" | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     from .cli import main | ||||
|  | ||||
|     main() | ||||
							
								
								
									
										6
									
								
								esphome/analyze_memory/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								esphome/analyze_memory/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| """Main entry point for running the memory analyzer as a module.""" | ||||
|  | ||||
| from .cli import main | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										421
									
								
								esphome/analyze_memory/cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								esphome/analyze_memory/cli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | ||||
| """CLI interface for memory analysis with report generation.""" | ||||
|  | ||||
| from collections import defaultdict | ||||
| import sys | ||||
|  | ||||
| from . import ( | ||||
|     _COMPONENT_API, | ||||
|     _COMPONENT_CORE, | ||||
|     _COMPONENT_PREFIX_ESPHOME, | ||||
|     _COMPONENT_PREFIX_EXTERNAL, | ||||
|     MemoryAnalyzer, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class MemoryAnalyzerCLI(MemoryAnalyzer): | ||||
|     """Memory analyzer with CLI-specific report generation.""" | ||||
|  | ||||
|     # Column width constants | ||||
|     COL_COMPONENT: int = 29 | ||||
|     COL_FLASH_TEXT: int = 14 | ||||
|     COL_FLASH_DATA: int = 14 | ||||
|     COL_RAM_DATA: int = 12 | ||||
|     COL_RAM_BSS: int = 12 | ||||
|     COL_TOTAL_FLASH: int = 15 | ||||
|     COL_TOTAL_RAM: int = 12 | ||||
|     COL_SEPARATOR: int = 3  # " | " | ||||
|  | ||||
|     # Core analysis column widths | ||||
|     COL_CORE_SUBCATEGORY: int = 30 | ||||
|     COL_CORE_SIZE: int = 12 | ||||
|     COL_CORE_COUNT: int = 6 | ||||
|     COL_CORE_PERCENT: int = 10 | ||||
|  | ||||
|     # Calculate table width once at class level | ||||
|     TABLE_WIDTH: int = ( | ||||
|         COL_COMPONENT | ||||
|         + COL_SEPARATOR | ||||
|         + COL_FLASH_TEXT | ||||
|         + COL_SEPARATOR | ||||
|         + COL_FLASH_DATA | ||||
|         + COL_SEPARATOR | ||||
|         + COL_RAM_DATA | ||||
|         + COL_SEPARATOR | ||||
|         + COL_RAM_BSS | ||||
|         + COL_SEPARATOR | ||||
|         + COL_TOTAL_FLASH | ||||
|         + COL_SEPARATOR | ||||
|         + COL_TOTAL_RAM | ||||
|     ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _make_separator_line(*widths: int) -> str: | ||||
|         """Create a separator line with given column widths. | ||||
|  | ||||
|         Args: | ||||
|             widths: Column widths to create separators for | ||||
|  | ||||
|         Returns: | ||||
|             Separator line like "----+---------+-----" | ||||
|         """ | ||||
|         return "-+-".join("-" * width for width in widths) | ||||
|  | ||||
|     # Pre-computed separator lines | ||||
|     MAIN_TABLE_SEPARATOR: str = _make_separator_line( | ||||
|         COL_COMPONENT, | ||||
|         COL_FLASH_TEXT, | ||||
|         COL_FLASH_DATA, | ||||
|         COL_RAM_DATA, | ||||
|         COL_RAM_BSS, | ||||
|         COL_TOTAL_FLASH, | ||||
|         COL_TOTAL_RAM, | ||||
|     ) | ||||
|  | ||||
|     CORE_TABLE_SEPARATOR: str = _make_separator_line( | ||||
|         COL_CORE_SUBCATEGORY, | ||||
|         COL_CORE_SIZE, | ||||
|         COL_CORE_COUNT, | ||||
|         COL_CORE_PERCENT, | ||||
|     ) | ||||
|  | ||||
|     def generate_report(self, detailed: bool = False) -> str: | ||||
|         """Generate a formatted memory report.""" | ||||
|         components = sorted( | ||||
|             self.components.items(), key=lambda x: x[1].flash_total, reverse=True | ||||
|         ) | ||||
|  | ||||
|         # Calculate totals | ||||
|         total_flash = sum(c.flash_total for _, c in components) | ||||
|         total_ram = sum(c.ram_total for _, c in components) | ||||
|  | ||||
|         # Build report | ||||
|         lines: list[str] = [] | ||||
|  | ||||
|         lines.append("=" * self.TABLE_WIDTH) | ||||
|         lines.append("Component Memory Analysis".center(self.TABLE_WIDTH)) | ||||
|         lines.append("=" * self.TABLE_WIDTH) | ||||
|         lines.append("") | ||||
|  | ||||
|         # Main table - fixed column widths | ||||
|         lines.append( | ||||
|             f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}" | ||||
|         ) | ||||
|         lines.append(self.MAIN_TABLE_SEPARATOR) | ||||
|  | ||||
|         for name, mem in components: | ||||
|             if mem.flash_total > 0 or mem.ram_total > 0: | ||||
|                 flash_rodata = mem.rodata_size + mem.data_size | ||||
|                 lines.append( | ||||
|                     f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | " | ||||
|                     f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | " | ||||
|                     f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B" | ||||
|                 ) | ||||
|  | ||||
|         lines.append(self.MAIN_TABLE_SEPARATOR) | ||||
|         lines.append( | ||||
|             f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | " | ||||
|             f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | " | ||||
|             f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B" | ||||
|         ) | ||||
|  | ||||
|         # Top consumers | ||||
|         lines.append("") | ||||
|         lines.append("Top Flash Consumers:") | ||||
|         for i, (name, mem) in enumerate(components[:25]): | ||||
|             if mem.flash_total > 0: | ||||
|                 percentage = ( | ||||
|                     (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 | ||||
|                 ) | ||||
|                 lines.append( | ||||
|                     f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" | ||||
|                 ) | ||||
|  | ||||
|         lines.append("") | ||||
|         lines.append("Top RAM Consumers:") | ||||
|         ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) | ||||
|         for i, (name, mem) in enumerate(ram_components[:25]): | ||||
|             if mem.ram_total > 0: | ||||
|                 percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 | ||||
|                 lines.append( | ||||
|                     f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" | ||||
|                 ) | ||||
|  | ||||
|         lines.append("") | ||||
|         lines.append( | ||||
|             "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." | ||||
|         ) | ||||
|         lines.append("=" * self.TABLE_WIDTH) | ||||
|  | ||||
|         # Add ESPHome core detailed analysis if there are core symbols | ||||
|         if self._esphome_core_symbols: | ||||
|             lines.append("") | ||||
|             lines.append("=" * self.TABLE_WIDTH) | ||||
|             lines.append( | ||||
|                 f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH) | ||||
|             ) | ||||
|             lines.append("=" * self.TABLE_WIDTH) | ||||
|             lines.append("") | ||||
|  | ||||
|             # Group core symbols by subcategory | ||||
|             core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( | ||||
|                 list | ||||
|             ) | ||||
|  | ||||
|             for symbol, demangled, size in self._esphome_core_symbols: | ||||
|                 # Categorize based on demangled name patterns | ||||
|                 subcategory = self._categorize_esphome_core_symbol(demangled) | ||||
|                 core_subcategories[subcategory].append((symbol, demangled, size)) | ||||
|  | ||||
|             # Sort subcategories by total size | ||||
|             sorted_subcategories = sorted( | ||||
|                 [ | ||||
|                     (name, symbols, sum(s[2] for s in symbols)) | ||||
|                     for name, symbols in core_subcategories.items() | ||||
|                 ], | ||||
|                 key=lambda x: x[2], | ||||
|                 reverse=True, | ||||
|             ) | ||||
|  | ||||
|             lines.append( | ||||
|                 f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | " | ||||
|                 f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}" | ||||
|             ) | ||||
|             lines.append(self.CORE_TABLE_SEPARATOR) | ||||
|  | ||||
|             core_total = sum(size for _, _, size in self._esphome_core_symbols) | ||||
|  | ||||
|             for subcategory, symbols, total_size in sorted_subcategories: | ||||
|                 percentage = (total_size / core_total * 100) if core_total > 0 else 0 | ||||
|                 lines.append( | ||||
|                     f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | " | ||||
|                     f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" | ||||
|                 ) | ||||
|  | ||||
|             # Top 15 largest core symbols | ||||
|             lines.append("") | ||||
|             lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") | ||||
|             sorted_core_symbols = sorted( | ||||
|                 self._esphome_core_symbols, key=lambda x: x[2], reverse=True | ||||
|             ) | ||||
|  | ||||
|             for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): | ||||
|                 lines.append(f"{i + 1}. {demangled} ({size:,} B)") | ||||
|  | ||||
|             lines.append("=" * self.TABLE_WIDTH) | ||||
|  | ||||
|         # Add detailed analysis for top ESPHome and external components | ||||
|         esphome_components = [ | ||||
|             (name, mem) | ||||
|             for name, mem in components | ||||
|             if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE | ||||
|         ] | ||||
|         external_components = [ | ||||
|             (name, mem) | ||||
|             for name, mem in components | ||||
|             if name.startswith(_COMPONENT_PREFIX_EXTERNAL) | ||||
|         ] | ||||
|  | ||||
|         top_esphome_components = sorted( | ||||
|             esphome_components, key=lambda x: x[1].flash_total, reverse=True | ||||
|         )[:30] | ||||
|  | ||||
|         # Include all external components (they're usually important) | ||||
|         top_external_components = sorted( | ||||
|             external_components, key=lambda x: x[1].flash_total, reverse=True | ||||
|         ) | ||||
|  | ||||
|         # Check if API component exists and ensure it's included | ||||
|         api_component = None | ||||
|         for name, mem in components: | ||||
|             if name == _COMPONENT_API: | ||||
|                 api_component = (name, mem) | ||||
|                 break | ||||
|  | ||||
|         # Also include wifi_stack and other important system components if they exist | ||||
|         system_components_to_include = [ | ||||
|             # Empty list - we've finished debugging symbol categorization | ||||
|             # Add component names here if you need to debug their symbols | ||||
|         ] | ||||
|         system_components = [ | ||||
|             (name, mem) | ||||
|             for name, mem in components | ||||
|             if name in system_components_to_include | ||||
|         ] | ||||
|  | ||||
|         # Combine all components to analyze: top ESPHome + all external + API if not already included + system components | ||||
|         components_to_analyze = ( | ||||
|             list(top_esphome_components) | ||||
|             + list(top_external_components) | ||||
|             + system_components | ||||
|         ) | ||||
|         if api_component and api_component not in components_to_analyze: | ||||
|             components_to_analyze.append(api_component) | ||||
|  | ||||
|         if components_to_analyze: | ||||
|             for comp_name, comp_mem in components_to_analyze: | ||||
|                 if not (comp_symbols := self._component_symbols.get(comp_name, [])): | ||||
|                     continue | ||||
|                 lines.append("") | ||||
|                 lines.append("=" * self.TABLE_WIDTH) | ||||
|                 lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH)) | ||||
|                 lines.append("=" * self.TABLE_WIDTH) | ||||
|                 lines.append("") | ||||
|  | ||||
|                 # Sort symbols by size | ||||
|                 sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True) | ||||
|  | ||||
|                 lines.append(f"Total symbols: {len(sorted_symbols)}") | ||||
|                 lines.append(f"Total size: {comp_mem.flash_total:,} B") | ||||
|                 lines.append("") | ||||
|  | ||||
|                 # Show all symbols > 100 bytes for better visibility | ||||
|                 large_symbols = [ | ||||
|                     (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 | ||||
|                 ] | ||||
|  | ||||
|                 lines.append( | ||||
|                     f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" | ||||
|                 ) | ||||
|                 for i, (symbol, demangled, size) in enumerate(large_symbols): | ||||
|                     lines.append(f"{i + 1}. {demangled} ({size:,} B)") | ||||
|  | ||||
|                 lines.append("=" * self.TABLE_WIDTH) | ||||
|  | ||||
|         return "\n".join(lines) | ||||
|  | ||||
|     def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: | ||||
|         """Dump uncategorized symbols for analysis.""" | ||||
|         # Sort by size descending | ||||
|         sorted_symbols = sorted( | ||||
|             self._uncategorized_symbols, key=lambda x: x[2], reverse=True | ||||
|         ) | ||||
|  | ||||
|         lines = ["Uncategorized Symbols Analysis", "=" * 80] | ||||
|         lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}") | ||||
|         lines.append( | ||||
|             f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes" | ||||
|         ) | ||||
|         lines.append("") | ||||
|         lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled") | ||||
|         lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) | ||||
|  | ||||
|         for symbol, demangled, size in sorted_symbols[:100]:  # Top 100 | ||||
|             demangled_display = ( | ||||
|                 demangled[:100] if symbol != demangled else "[not demangled]" | ||||
|             ) | ||||
|             lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}") | ||||
|  | ||||
|         if len(sorted_symbols) > 100: | ||||
|             lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") | ||||
|  | ||||
|         content = "\n".join(lines) | ||||
|  | ||||
|         if output_file: | ||||
|             with open(output_file, "w", encoding="utf-8") as f: | ||||
|                 f.write(content) | ||||
|         else: | ||||
|             print(content) | ||||
|  | ||||
|  | ||||
| def analyze_elf( | ||||
|     elf_path: str, | ||||
|     objdump_path: str | None = None, | ||||
|     readelf_path: str | None = None, | ||||
|     detailed: bool = False, | ||||
|     external_components: set[str] | None = None, | ||||
| ) -> str: | ||||
|     """Analyze an ELF file and return a memory report.""" | ||||
|     analyzer = MemoryAnalyzerCLI( | ||||
|         elf_path, objdump_path, readelf_path, external_components | ||||
|     ) | ||||
|     analyzer.analyze() | ||||
|     return analyzer.generate_report(detailed) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     """CLI entrypoint for memory analysis.""" | ||||
|     if len(sys.argv) < 2: | ||||
|         print("Usage: python -m esphome.analyze_memory <build_directory>") | ||||
|         print("\nAnalyze memory usage from an ESPHome build directory.") | ||||
|         print("The build directory should contain firmware.elf and idedata will be") | ||||
|         print("loaded from ~/.esphome/.internal/idedata/<device>.json") | ||||
|         print("\nExamples:") | ||||
|         print("  python -m esphome.analyze_memory ~/.esphome/build/my-device") | ||||
|         print("  python -m esphome.analyze_memory .esphome/build/my-device") | ||||
|         print("  python -m esphome.analyze_memory my-device  # Short form") | ||||
|         sys.exit(1) | ||||
|  | ||||
|     build_dir = sys.argv[1] | ||||
|  | ||||
|     # Load build directory | ||||
|     import json | ||||
|     from pathlib import Path | ||||
|  | ||||
|     from esphome.platformio_api import IDEData | ||||
|  | ||||
|     build_path = Path(build_dir) | ||||
|  | ||||
|     # If no path separator in name, assume it's a device name | ||||
|     if "/" not in build_dir and not build_path.is_dir(): | ||||
|         # Try current directory first | ||||
|         cwd_path = Path.cwd() / ".esphome" / "build" / build_dir | ||||
|         if cwd_path.is_dir(): | ||||
|             build_path = cwd_path | ||||
|             print(f"Using build directory: {build_path}", file=sys.stderr) | ||||
|         else: | ||||
|             # Fall back to home directory | ||||
|             build_path = Path.home() / ".esphome" / "build" / build_dir | ||||
|             print(f"Using build directory: {build_path}", file=sys.stderr) | ||||
|  | ||||
|     if not build_path.is_dir(): | ||||
|         print(f"Error: {build_path} is not a directory", file=sys.stderr) | ||||
|         sys.exit(1) | ||||
|  | ||||
|     # Find firmware.elf | ||||
|     elf_file = None | ||||
|     for elf_candidate in [ | ||||
|         build_path / "firmware.elf", | ||||
|         build_path / ".pioenvs" / build_path.name / "firmware.elf", | ||||
|     ]: | ||||
|         if elf_candidate.exists(): | ||||
|             elf_file = str(elf_candidate) | ||||
|             break | ||||
|  | ||||
|     if not elf_file: | ||||
|         print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr) | ||||
|         sys.exit(1) | ||||
|  | ||||
|     # Find idedata.json - check current directory first, then home | ||||
|     device_name = build_path.name | ||||
|     idedata_candidates = [ | ||||
|         Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json", | ||||
|         Path.home() / ".esphome" / "idedata" / f"{device_name}.json", | ||||
|     ] | ||||
|  | ||||
|     idedata = None | ||||
|     for idedata_path in idedata_candidates: | ||||
|         if not idedata_path.exists(): | ||||
|             continue | ||||
|         try: | ||||
|             with open(idedata_path, encoding="utf-8") as f: | ||||
|                 raw_data = json.load(f) | ||||
|             idedata = IDEData(raw_data) | ||||
|             print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) | ||||
|             break | ||||
|         except (json.JSONDecodeError, OSError) as e: | ||||
|             print(f"Warning: Failed to load idedata: {e}", file=sys.stderr) | ||||
|  | ||||
|     if not idedata: | ||||
|         print( | ||||
|             f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})", | ||||
|             file=sys.stderr, | ||||
|         ) | ||||
|  | ||||
|     analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata) | ||||
|     analyzer.analyze() | ||||
|     report = analyzer.generate_report() | ||||
|     print(report) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										1052
									
								
								esphome/analyze_memory/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1052
									
								
								esphome/analyze_memory/const.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										121
									
								
								esphome/analyze_memory/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/analyze_memory/helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| """Helper functions for memory analysis.""" | ||||
|  | ||||
| from functools import cache | ||||
| from pathlib import Path | ||||
|  | ||||
| from .const import SECTION_MAPPING | ||||
|  | ||||
| # Import namespace constant from parent module | ||||
| # Note: This would create a circular import if done at module level, | ||||
| # so we'll define it locally here as well | ||||
| _NAMESPACE_ESPHOME = "esphome::" | ||||
|  | ||||
|  | ||||
| # Get the list of actual ESPHome components by scanning the components directory | ||||
| @cache | ||||
| def get_esphome_components(): | ||||
|     """Get set of actual ESPHome components from the components directory.""" | ||||
|     # Find the components directory relative to this file | ||||
|     # Go up two levels from analyze_memory/helpers.py to esphome/ | ||||
|     current_dir = Path(__file__).parent.parent | ||||
|     components_dir = current_dir / "components" | ||||
|  | ||||
|     if not components_dir.exists() or not components_dir.is_dir(): | ||||
|         return frozenset() | ||||
|  | ||||
|     return frozenset( | ||||
|         item.name | ||||
|         for item in components_dir.iterdir() | ||||
|         if item.is_dir() | ||||
|         and not item.name.startswith(".") | ||||
|         and not item.name.startswith("__") | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @cache | ||||
| def get_component_class_patterns(component_name: str) -> list[str]: | ||||
|     """Generate component class name patterns for symbol matching. | ||||
|  | ||||
|     Args: | ||||
|         component_name: The component name (e.g., "ota", "wifi", "api") | ||||
|  | ||||
|     Returns: | ||||
|         List of pattern strings to match against demangled symbols | ||||
|     """ | ||||
|     component_upper = component_name.upper() | ||||
|     component_camel = component_name.replace("_", "").title() | ||||
|     return [ | ||||
|         f"{_NAMESPACE_ESPHOME}{component_upper}Component",  # e.g., esphome::OTAComponent | ||||
|         f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component",  # e.g., esphome::ESPHomeOTAComponent | ||||
|         f"{_NAMESPACE_ESPHOME}{component_camel}Component",  # e.g., esphome::OtaComponent | ||||
|         f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component",  # e.g., esphome::ESPHomeOtaComponent | ||||
|     ] | ||||
|  | ||||
|  | ||||
| def map_section_name(raw_section: str) -> str | None: | ||||
|     """Map raw section name to standard section. | ||||
|  | ||||
|     Args: | ||||
|         raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1") | ||||
|  | ||||
|     Returns: | ||||
|         Standard section name (".text", ".rodata", ".data", ".bss") or None | ||||
|     """ | ||||
|     for standard_section, patterns in SECTION_MAPPING.items(): | ||||
|         if any(pattern in raw_section for pattern in patterns): | ||||
|             return standard_section | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: | ||||
|     """Parse a single symbol line from objdump output. | ||||
|  | ||||
|     Args: | ||||
|         line: Line from objdump -t output | ||||
|  | ||||
|     Returns: | ||||
|         Tuple of (section, name, size, address) or None if not a valid symbol. | ||||
|         Format: address l/g w/d F/O section size name | ||||
|         Example: 40084870 l     F .iram0.text    00000000 _xt_user_exc | ||||
|     """ | ||||
|     parts = line.split() | ||||
|     if len(parts) < 5: | ||||
|         return None | ||||
|  | ||||
|     try: | ||||
|         # Validate and extract address | ||||
|         address = parts[0] | ||||
|         int(address, 16) | ||||
|     except ValueError: | ||||
|         return None | ||||
|  | ||||
|     # Look for F (function) or O (object) flag | ||||
|     if "F" not in parts and "O" not in parts: | ||||
|         return None | ||||
|  | ||||
|     # Find section, size, and name | ||||
|     for i, part in enumerate(parts): | ||||
|         if not part.startswith("."): | ||||
|             continue | ||||
|  | ||||
|         section = map_section_name(part) | ||||
|         if not section: | ||||
|             break | ||||
|  | ||||
|         # Need at least size field after section | ||||
|         if i + 1 >= len(parts): | ||||
|             break | ||||
|  | ||||
|         try: | ||||
|             size = int(parts[i + 1], 16) | ||||
|         except ValueError: | ||||
|             break | ||||
|  | ||||
|         # Need symbol name and non-zero size | ||||
|         if i + 2 >= len(parts) or size == 0: | ||||
|             break | ||||
|  | ||||
|         name = " ".join(parts[i + 2 :]) | ||||
|         return (section, name, size, address) | ||||
|  | ||||
|     return None | ||||
| @@ -16,7 +16,12 @@ from esphome.const import ( | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType | ||||
| from esphome.cpp_generator import ( | ||||
|     LambdaExpression, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     TemplateArgsType, | ||||
| ) | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import ConfigType | ||||
| from esphome.util import Registry | ||||
| @@ -87,6 +92,7 @@ def validate_potentially_or_condition(value): | ||||
|  | ||||
| DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| WhileAction = cg.esphome_ns.class_("WhileAction", Action) | ||||
| RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) | ||||
| @@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) | ||||
| Automation = cg.esphome_ns.class_("Automation") | ||||
|  | ||||
| LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) | ||||
| StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) | ||||
| ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) | ||||
|  | ||||
|  | ||||
| def new_lambda_pvariable( | ||||
|     id_obj: ID, | ||||
|     lambda_expr: LambdaExpression, | ||||
|     stateless_class: MockObjClass, | ||||
|     template_arg: cg.TemplateArguments | None = None, | ||||
| ) -> MockObj: | ||||
|     """Create Pvariable for lambda, using stateless class if applicable. | ||||
|  | ||||
|     Combines ID selection and Pvariable creation in one call. For stateless | ||||
|     lambdas (empty capture), uses function pointer instead of std::function. | ||||
|  | ||||
|     Args: | ||||
|         id_obj: The ID object (action_id, condition_id, or filter_id) | ||||
|         lambda_expr: The lambda expression object | ||||
|         stateless_class: The stateless class to use for stateless lambdas | ||||
|         template_arg: Optional template arguments (for actions/conditions) | ||||
|  | ||||
|     Returns: | ||||
|         The created Pvariable | ||||
|     """ | ||||
|     # For stateless lambdas, use function pointer instead of std::function | ||||
|     if lambda_expr.capture == "": | ||||
|         id_obj = id_obj.copy() | ||||
|         id_obj.type = stateless_class | ||||
|  | ||||
|     if template_arg is not None: | ||||
|         return cg.new_Pvariable(id_obj, template_arg, lambda_expr) | ||||
|     return cg.new_Pvariable(id_obj, lambda_expr) | ||||
|  | ||||
|  | ||||
| def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|     if extra_schema is None: | ||||
|         extra_schema = {} | ||||
| @@ -240,7 +277,9 @@ async def lambda_condition_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=bool) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable( | ||||
|         condition_id, lambda_, StatelessLambdaCondition, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @register_condition( | ||||
| @@ -406,7 +445,7 @@ async def lambda_action_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) | ||||
|  | ||||
|  | ||||
| @register_action( | ||||
|   | ||||
| @@ -62,6 +62,7 @@ from esphome.cpp_types import (  # noqa: F401 | ||||
|     EntityBase, | ||||
|     EntityCategory, | ||||
|     ESPTime, | ||||
|     FixedVector, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     JsonObject, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; | ||||
| static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; | ||||
| static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; | ||||
|  | ||||
| AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
| AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| void AdalightLightEffect::start() { | ||||
|   AddressableLightEffect::start(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ namespace adalight { | ||||
|  | ||||
| class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { | ||||
|  public: | ||||
|   AdalightLightEffect(const std::string &name); | ||||
|   AdalightLightEffect(const char *name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -172,12 +172,6 @@ def alarm_control_panel_schema( | ||||
|     return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel) | ||||
| ALARM_CONTROL_PANEL_SCHEMA.add_extra( | ||||
|     cv.deprecated_schema_constant("alarm_control_panel") | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode | ||||
|   void dump_config() override; | ||||
|   climate::ClimateTraits traits() override { | ||||
|     auto traits = climate::ClimateTraits(); | ||||
|     traits.set_supports_current_temperature(true); | ||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|     traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); | ||||
|     traits.set_visual_min_temperature(25.0); | ||||
|     traits.set_visual_max_temperature(100.0); | ||||
|   | ||||
| @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
|     "int": cg.int32, | ||||
|     "float": float, | ||||
|     "string": cg.std_string, | ||||
|     "bool[]": cg.std_vector.template(bool), | ||||
|     "int[]": cg.std_vector.template(cg.int32), | ||||
|     "float[]": cg.std_vector.template(float), | ||||
|     "string[]": cg.std_vector.template(cg.std_string), | ||||
|     "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), | ||||
|     "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), | ||||
|     "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), | ||||
|     "string[]": cg.FixedVector.template(cg.std_string) | ||||
|     .operator("const") | ||||
|     .operator("ref"), | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| @@ -155,6 +157,17 @@ def _validate_api_config(config: ConfigType) -> ConfigType: | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def _consume_api_sockets(config: ConfigType) -> ConfigType: | ||||
|     """Register socket needs for API component.""" | ||||
|     from esphome.components import socket | ||||
|  | ||||
|     # API needs 1 listening socket + typically 3 concurrent client connections | ||||
|     # (not max_connections, which is the upper limit rarely reached) | ||||
|     sockets_needed = 1 + 3 | ||||
|     socket.consume_sockets(sockets_needed, "api")(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -222,6 +235,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.rename_key(CONF_SERVICES, CONF_ACTIONS), | ||||
|     _validate_api_config, | ||||
|     _consume_api_sockets, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -246,6 +260,10 @@ async def to_code(config): | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration | ||||
|     if config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_CUSTOM_SERVICES") | ||||
|  | ||||
|     if config[CONF_HOMEASSISTANT_SERVICES]: | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|  | ||||
| @@ -253,6 +271,8 @@ async def to_code(config): | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_STATES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         # Collect all triggers first, then register all at once with initializer_list | ||||
|         triggers: list[cg.Pvariable] = [] | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
|             func_args = [] | ||||
| @@ -266,8 +286,10 @@ async def to_code(config): | ||||
|             trigger = cg.new_Pvariable( | ||||
|                 conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names | ||||
|             ) | ||||
|             cg.add(var.register_user_service(trigger)) | ||||
|             triggers.append(trigger) | ||||
|             await automation.build_automation(trigger, func_args, conf) | ||||
|         # Register all services at once - single allocation, no reallocations | ||||
|         cg.add(var.initialize_user_services(triggers)) | ||||
|  | ||||
|     if CONF_ON_CLIENT_CONNECTED in config: | ||||
|         cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") | ||||
| @@ -380,12 +402,19 @@ async def homeassistant_service_to_code( | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, False) | ||||
|     templ = await cg.templatable(config[CONF_ACTION], args, None) | ||||
|     cg.add(var.set_service(templ)) | ||||
|  | ||||
|     # Initialize FixedVectors with exact sizes from config | ||||
|     cg.add(var.init_data(len(config[CONF_DATA]))) | ||||
|     for key, value in config[CONF_DATA].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_data(key, templ)) | ||||
|  | ||||
|     cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) | ||||
|     for key, value in config[CONF_DATA_TEMPLATE].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_data_template(key, templ)) | ||||
|  | ||||
|     cg.add(var.init_variables(len(config[CONF_VARIABLES]))) | ||||
|     for key, value in config[CONF_VARIABLES].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_variable(key, templ)) | ||||
| @@ -458,15 +487,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args): | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, True) | ||||
|     templ = await cg.templatable(config[CONF_EVENT], args, None) | ||||
|     cg.add(var.set_service(templ)) | ||||
|  | ||||
|     # Initialize FixedVectors with exact sizes from config | ||||
|     cg.add(var.init_data(len(config[CONF_DATA]))) | ||||
|     for key, value in config[CONF_DATA].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_data(key, templ)) | ||||
|  | ||||
|     cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) | ||||
|     for key, value in config[CONF_DATA_TEMPLATE].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_data_template(key, templ)) | ||||
|  | ||||
|     cg.add(var.init_variables(len(config[CONF_VARIABLES]))) | ||||
|     for key, value in config[CONF_VARIABLES].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_variable(key, templ)) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @@ -489,6 +526,8 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg | ||||
|     serv = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, True) | ||||
|     cg.add(var.set_service("esphome.tag_scanned")) | ||||
|     # Initialize FixedVector with exact size (1 data field) | ||||
|     cg.add(var.init_data(1)) | ||||
|     templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) | ||||
|     cg.add(var.add_data("tag_id", templ)) | ||||
|     return var | ||||
|   | ||||
| @@ -506,7 +506,7 @@ message ListEntitiesLightResponse { | ||||
|   string name = 3; | ||||
|   reserved 4; // Deprecated: was string unique_id | ||||
|  | ||||
|   repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"]; | ||||
|   repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"]; | ||||
|   // next four supports_* are for legacy clients, newer clients should use color modes | ||||
|   // Deprecated in API version 1.6 | ||||
|   bool legacy_supports_brightness = 5 [deprecated=true]; | ||||
| @@ -776,9 +776,9 @@ message HomeassistantActionRequest { | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; | ||||
|  | ||||
|   string service = 1; | ||||
|   repeated HomeassistantServiceMap data = 2; | ||||
|   repeated HomeassistantServiceMap data_template = 3; | ||||
|   repeated HomeassistantServiceMap variables = 4; | ||||
|   repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true]; | ||||
|   repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true]; | ||||
|   repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true]; | ||||
|   bool is_event = 5; | ||||
|   uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; | ||||
|   bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; | ||||
| @@ -866,7 +866,7 @@ message ListEntitiesServicesResponse { | ||||
|  | ||||
|   string name = 1; | ||||
|   fixed32 key = 2; | ||||
|   repeated ListEntitiesServicesArgument args = 3; | ||||
|   repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; | ||||
| } | ||||
| message ExecuteServiceArgument { | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
| @@ -876,10 +876,10 @@ message ExecuteServiceArgument { | ||||
|   string string_ = 4; | ||||
|   // ESPHome 1.14 (api v1.3) make int a signed value | ||||
|   sint32 int_ = 5; | ||||
|   repeated bool bool_array = 6 [packed=false]; | ||||
|   repeated sint32 int_array = 7 [packed=false]; | ||||
|   repeated float float_array = 8 [packed=false]; | ||||
|   repeated string string_array = 9; | ||||
|   repeated bool bool_array = 6 [packed=false, (fixed_vector) = true]; | ||||
|   repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true]; | ||||
|   repeated float float_array = 8 [packed=false, (fixed_vector) = true]; | ||||
|   repeated string string_array = 9 [(fixed_vector) = true]; | ||||
| } | ||||
| message ExecuteServiceRequest { | ||||
|   option (id) = 42; | ||||
| @@ -888,7 +888,7 @@ message ExecuteServiceRequest { | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   repeated ExecuteServiceArgument args = 2; | ||||
|   repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; | ||||
| } | ||||
|  | ||||
| // ==================== CAMERA ==================== | ||||
| @@ -987,9 +987,9 @@ message ListEntitiesClimateResponse { | ||||
|   string name = 3; | ||||
|   reserved 4; // Deprecated: was string unique_id | ||||
|  | ||||
|   bool supports_current_temperature = 5; | ||||
|   bool supports_two_point_target_temperature = 6; | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; | ||||
|   bool supports_current_temperature = 5; // Deprecated: use feature_flags | ||||
|   bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; | ||||
|   float visual_min_temperature = 8; | ||||
|   float visual_max_temperature = 9; | ||||
|   float visual_target_temperature_step = 10; | ||||
| @@ -997,21 +997,22 @@ message ListEntitiesClimateResponse { | ||||
|   // is if CLIMATE_PRESET_AWAY exists is supported_presets | ||||
|   // Deprecated in API version 1.5 | ||||
|   bool legacy_supports_away = 11 [deprecated=true]; | ||||
|   bool supports_action = 12; | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; | ||||
|   bool supports_action = 12; // Deprecated: use feature_flags | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 20; | ||||
|   float visual_current_temperature_step = 21; | ||||
|   bool supports_current_humidity = 22; | ||||
|   bool supports_target_humidity = 23; | ||||
|   bool supports_current_humidity = 22; // Deprecated: use feature_flags | ||||
|   bool supports_target_humidity = 23; // Deprecated: use feature_flags | ||||
|   float visual_min_humidity = 24; | ||||
|   float visual_max_humidity = 25; | ||||
|   uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"]; | ||||
|   uint32 feature_flags = 27; | ||||
| } | ||||
| message ClimateStateResponse { | ||||
|   option (id) = 47; | ||||
| @@ -1142,7 +1143,7 @@ message ListEntitiesSelectResponse { | ||||
|   reserved 4; // Deprecated: was string unique_id | ||||
|  | ||||
|   string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   repeated string options = 6 [(container_pointer) = "std::vector"]; | ||||
|   repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; | ||||
| @@ -1519,7 +1520,7 @@ message BluetoothGATTCharacteristic { | ||||
|   repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; | ||||
|   uint32 handle = 2; | ||||
|   uint32 properties = 3; | ||||
|   repeated BluetoothGATTDescriptor descriptors = 4; | ||||
|   repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true]; | ||||
|  | ||||
|   // New field for efficient UUID (v1.12+) | ||||
|   // Only one of uuid or short_uuid will be set. | ||||
| @@ -1531,7 +1532,7 @@ message BluetoothGATTCharacteristic { | ||||
| message BluetoothGATTService { | ||||
|   repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; | ||||
|   uint32 handle = 2; | ||||
|   repeated BluetoothGATTCharacteristic characteristics = 3; | ||||
|   repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true]; | ||||
|  | ||||
|   // New field for efficient UUID (v1.12+) | ||||
|   // Only one of uuid or short_uuid will be set. | ||||
|   | ||||
| @@ -27,6 +27,9 @@ | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
| #include "esphome/components/climate/climate_mode.h" | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| #include "esphome/components/voice_assistant/voice_assistant.h" | ||||
| #endif | ||||
| @@ -450,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * | ||||
|                                              bool is_single) { | ||||
|   auto *light = static_cast<light::LightState *>(entity); | ||||
|   LightStateResponse resp; | ||||
|   auto traits = light->get_traits(); | ||||
|   auto values = light->remote_values; | ||||
|   auto color_mode = values.get_color_mode(); | ||||
|   resp.state = values.is_on(); | ||||
| @@ -474,7 +476,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | ||||
|   auto *light = static_cast<light::LightState *>(entity); | ||||
|   ListEntitiesLightResponse msg; | ||||
|   auto traits = light->get_traits(); | ||||
|   msg.supported_color_modes = &traits.get_supported_color_modes_for_api_(); | ||||
|   // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values | ||||
|   msg.supported_color_modes = &traits.get_supported_color_modes(); | ||||
|   if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || | ||||
|       traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { | ||||
|     msg.min_mireds = traits.get_min_mireds(); | ||||
| @@ -483,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | ||||
|   if (light->supports_effects()) { | ||||
|     msg.effects.emplace_back("None"); | ||||
|     for (auto *effect : light->get_effects()) { | ||||
|       msg.effects.push_back(effect->get_name()); | ||||
|       msg.effects.emplace_back(effect->get_name()); | ||||
|     } | ||||
|   } | ||||
|   return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
| @@ -623,9 +626,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection | ||||
|   auto traits = climate->get_traits(); | ||||
|   resp.mode = static_cast<enums::ClimateMode>(climate->mode); | ||||
|   resp.action = static_cast<enums::ClimateAction>(climate->action); | ||||
|   if (traits.get_supports_current_temperature()) | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) | ||||
|     resp.current_temperature = climate->current_temperature; | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     resp.target_temperature_low = climate->target_temperature_low; | ||||
|     resp.target_temperature_high = climate->target_temperature_high; | ||||
|   } else { | ||||
| @@ -644,9 +648,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) | ||||
|     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); | ||||
|   if (traits.get_supports_current_humidity()) | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) | ||||
|     resp.current_humidity = climate->current_humidity; | ||||
|   if (traits.get_supports_target_humidity()) | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) | ||||
|     resp.target_humidity = climate->target_humidity; | ||||
|   return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                       is_single); | ||||
| @@ -656,23 +660,27 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection | ||||
|   auto *climate = static_cast<climate::Climate *>(entity); | ||||
|   ListEntitiesClimateResponse msg; | ||||
|   auto traits = climate->get_traits(); | ||||
|   msg.supports_current_temperature = traits.get_supports_current_temperature(); | ||||
|   msg.supports_current_humidity = traits.get_supports_current_humidity(); | ||||
|   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); | ||||
|   msg.supports_target_humidity = traits.get_supports_target_humidity(); | ||||
|   msg.supported_modes = &traits.get_supported_modes_for_api_(); | ||||
|   // Flags set for backward compatibility, deprecated in 2025.11.0 | ||||
|   msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|   msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); | ||||
|   msg.supports_two_point_target_temperature = traits.has_feature_flags( | ||||
|       climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); | ||||
|   msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY); | ||||
|   msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); | ||||
|   // Current feature flags and other supported parameters | ||||
|   msg.feature_flags = traits.get_feature_flags(); | ||||
|   msg.supported_modes = &traits.get_supported_modes(); | ||||
|   msg.visual_min_temperature = traits.get_visual_min_temperature(); | ||||
|   msg.visual_max_temperature = traits.get_visual_max_temperature(); | ||||
|   msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); | ||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||
|   msg.visual_min_humidity = traits.get_visual_min_humidity(); | ||||
|   msg.visual_max_humidity = traits.get_visual_max_humidity(); | ||||
|   msg.supports_action = traits.get_supports_action(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); | ||||
|   msg.supported_presets = &traits.get_supported_presets_for_api_(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); | ||||
|   msg.supported_presets = &traits.get_supported_presets(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes(); | ||||
|   return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -1074,13 +1082,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { | ||||
|     homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); | ||||
| #ifdef USE_TIME_TIMEZONE | ||||
|     if (value.timezone_len > 0) { | ||||
|       const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone(); | ||||
|       // Compare without allocating a string | ||||
|       if (current_tz.length() != value.timezone_len || | ||||
|           memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) { | ||||
|         homeassistant::global_homeassistant_time->set_timezone( | ||||
|             std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len)); | ||||
|       } | ||||
|       homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone), | ||||
|                                                              value.timezone_len); | ||||
|     } | ||||
| #endif | ||||
|   } | ||||
| @@ -1406,7 +1409,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { | ||||
|  | ||||
|   HelloResponse resp; | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 12; | ||||
|   resp.api_version_minor = 13; | ||||
|   // Send only the version string - the client only logs this for debugging and doesn't use it otherwise | ||||
|   resp.set_server_info(ESPHOME_VERSION_REF); | ||||
|   resp.set_name(StringRef(App.get_name())); | ||||
| @@ -1569,7 +1572,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption | ||||
|   resp.success = false; | ||||
|  | ||||
|   psk_t psk{}; | ||||
|   if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|   if (msg.key.empty()) { | ||||
|     if (this->parent_->clear_noise_psk(true)) { | ||||
|       resp.success = true; | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Failed to clear encryption key"); | ||||
|     } | ||||
|   } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|     ESP_LOGW(TAG, "Invalid encryption key length"); | ||||
|   } else if (!this->parent_->save_noise_psk(psk, true)) { | ||||
|     ESP_LOGW(TAG, "Failed to save encryption key"); | ||||
|   | ||||
| @@ -242,7 +242,6 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|     const std::string &name = App.get_name(); | ||||
|     const std::string &mac = get_mac_address(); | ||||
|  | ||||
|     std::vector<uint8_t> msg; | ||||
|     // Calculate positions and sizes | ||||
|     size_t name_len = name.size() + 1;  // including null terminator | ||||
|     size_t mac_len = mac.size() + 1;    // including null terminator | ||||
| @@ -250,17 +249,17 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|     size_t mac_offset = name_offset + name_len; | ||||
|     size_t total_size = 1 + name_len + mac_len; | ||||
|  | ||||
|     msg.resize(total_size); | ||||
|     auto msg = std::make_unique<uint8_t[]>(total_size); | ||||
|  | ||||
|     // chosen proto | ||||
|     msg[0] = 0x01; | ||||
|  | ||||
|     // node name, terminated by null byte | ||||
|     std::memcpy(msg.data() + name_offset, name.c_str(), name_len); | ||||
|     std::memcpy(msg.get() + name_offset, name.c_str(), name_len); | ||||
|     // node mac, terminated by null byte | ||||
|     std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len); | ||||
|     std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len); | ||||
|  | ||||
|     aerr = write_frame_(msg.data(), msg.size()); | ||||
|     aerr = write_frame_(msg.get(), total_size); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
| @@ -339,32 +338,32 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso | ||||
| #ifdef USE_STORE_LOG_STR_IN_FLASH | ||||
|   // On ESP8266 with flash strings, we need to use PROGMEM-aware functions | ||||
|   size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); | ||||
|   std::vector<uint8_t> data; | ||||
|   data.resize(reason_len + 1); | ||||
|   size_t data_size = reason_len + 1; | ||||
|   auto data = std::make_unique<uint8_t[]>(data_size); | ||||
|   data[0] = 0x01;  // failure | ||||
|  | ||||
|   // Copy error message from PROGMEM | ||||
|   if (reason_len > 0) { | ||||
|     memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len); | ||||
|     memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len); | ||||
|   } | ||||
| #else | ||||
|   // Normal memory access | ||||
|   const char *reason_str = LOG_STR_ARG(reason); | ||||
|   size_t reason_len = strlen(reason_str); | ||||
|   std::vector<uint8_t> data; | ||||
|   data.resize(reason_len + 1); | ||||
|   size_t data_size = reason_len + 1; | ||||
|   auto data = std::make_unique<uint8_t[]>(data_size); | ||||
|   data[0] = 0x01;  // failure | ||||
|  | ||||
|   // Copy error message in bulk | ||||
|   if (reason_len > 0) { | ||||
|     std::memcpy(data.data() + 1, reason_str, reason_len); | ||||
|     std::memcpy(data.get() + 1, reason_str, reason_len); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // temporarily remove failed state | ||||
|   auto orig_state = state_; | ||||
|   state_ = State::EXPLICIT_REJECT; | ||||
|   write_frame_(data.data(), data.size()); | ||||
|   write_frame_(data.get(), data_size); | ||||
|   state_ = orig_state; | ||||
| } | ||||
| APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   | ||||
| @@ -64,4 +64,20 @@ extend google.protobuf.FieldOptions { | ||||
|     // This is typically done through methods returning const T& or special accessor | ||||
|     // methods like get_options() or supported_modes_for_api_(). | ||||
|     optional string container_pointer = 50001; | ||||
|  | ||||
|     // fixed_vector: Use FixedVector instead of std::vector for repeated fields | ||||
|     // When set, the repeated field will use FixedVector<T> which requires calling | ||||
|     // init(size) before adding elements. This eliminates std::vector template overhead | ||||
|     // and is ideal when the exact size is known before populating the array. | ||||
|     optional bool fixed_vector = 50013 [default=false]; | ||||
|  | ||||
|     // container_pointer_no_template: Use a non-template container type for repeated fields | ||||
|     // Similar to container_pointer, but for containers that don't take template parameters. | ||||
|     // The container type is used as-is without appending element type. | ||||
|     // The container must have: | ||||
|     // - begin() and end() methods returning iterators | ||||
|     // - empty() method | ||||
|     // Example: [(container_pointer_no_template) = "light::ColorModeMask"] | ||||
|     //   generates: const light::ColorModeMask *supported_color_modes{}; | ||||
|     optional string container_pointer_no_template = 50014; | ||||
| } | ||||
|   | ||||
| @@ -1064,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { | ||||
|   uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6); | ||||
|   this->bool_array.init(count_bool_array); | ||||
|   uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7); | ||||
|   this->int_array.init(count_int_array); | ||||
|   uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8); | ||||
|   this->float_array.init(count_float_array); | ||||
|   uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9); | ||||
|   this->string_array.init(count_string_array); | ||||
|   ProtoDecodableMessage::decode(buffer, length); | ||||
| } | ||||
| bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 2: | ||||
| @@ -1085,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { | ||||
|   uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2); | ||||
|   this->args.init(count_args); | ||||
|   ProtoDecodableMessage::decode(buffer, length); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
| void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { | ||||
| @@ -1185,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
| #ifdef USE_DEVICES | ||||
|   buffer.encode_uint32(26, this->device_id); | ||||
| #endif | ||||
|   buffer.encode_uint32(27, this->feature_flags); | ||||
| } | ||||
| void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { | ||||
|   size.add_length(1, this->object_id_ref_.size()); | ||||
| @@ -1239,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { | ||||
| #ifdef USE_DEVICES | ||||
|   size.add_uint32(2, this->device_id); | ||||
| #endif | ||||
|   size.add_uint32(2, this->feature_flags); | ||||
| } | ||||
| void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
| @@ -1457,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { | ||||
| #ifdef USE_ENTITY_ICON | ||||
|   buffer.encode_string(5, this->icon_ref_); | ||||
| #endif | ||||
|   for (const auto &it : *this->options) { | ||||
|     buffer.encode_string(6, it, true); | ||||
|   for (const char *it : *this->options) { | ||||
|     buffer.encode_string(6, it, strlen(it), true); | ||||
|   } | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); | ||||
| @@ -1474,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { | ||||
|   size.add_length(1, this->icon_ref_.size()); | ||||
| #endif | ||||
|   if (!this->options->empty()) { | ||||
|     for (const auto &it : *this->options) { | ||||
|       size.add_length_force(1, it.size()); | ||||
|     for (const char *it : *this->options) { | ||||
|       size.add_length_force(1, strlen(it)); | ||||
|     } | ||||
|   } | ||||
|   size.add_bool(1, this->disabled_by_default); | ||||
|   | ||||
| @@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_light_response"; } | ||||
| #endif | ||||
|   const std::set<light::ColorMode> *supported_color_modes{}; | ||||
|   const light::ColorModeMask *supported_color_modes{}; | ||||
|   float min_mireds{0.0f}; | ||||
|   float max_mireds{0.0f}; | ||||
|   std::vector<std::string> effects{}; | ||||
| @@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage { | ||||
| #endif | ||||
|   StringRef service_ref_{}; | ||||
|   void set_service(const StringRef &ref) { this->service_ref_ = ref; } | ||||
|   std::vector<HomeassistantServiceMap> data{}; | ||||
|   std::vector<HomeassistantServiceMap> data_template{}; | ||||
|   std::vector<HomeassistantServiceMap> variables{}; | ||||
|   FixedVector<HomeassistantServiceMap> data{}; | ||||
|   FixedVector<HomeassistantServiceMap> data_template{}; | ||||
|   FixedVector<HomeassistantServiceMap> variables{}; | ||||
|   bool is_event{false}; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   uint32_t call_id{0}; | ||||
| @@ -1263,7 +1263,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { | ||||
|   StringRef name_ref_{}; | ||||
|   void set_name(const StringRef &ref) { this->name_ref_ = ref; } | ||||
|   uint32_t key{0}; | ||||
|   std::vector<ListEntitiesServicesArgument> args{}; | ||||
|   FixedVector<ListEntitiesServicesArgument> args{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1279,10 +1279,11 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { | ||||
|   float float_{0.0f}; | ||||
|   std::string string_{}; | ||||
|   int32_t int_{0}; | ||||
|   std::vector<bool> bool_array{}; | ||||
|   std::vector<int32_t> int_array{}; | ||||
|   std::vector<float> float_array{}; | ||||
|   std::vector<std::string> string_array{}; | ||||
|   FixedVector<bool> bool_array{}; | ||||
|   FixedVector<int32_t> int_array{}; | ||||
|   FixedVector<float> float_array{}; | ||||
|   FixedVector<std::string> string_array{}; | ||||
|   void decode(const uint8_t *buffer, size_t length) override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -1300,7 +1301,8 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { | ||||
|   const char *message_name() const override { return "execute_service_request"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   std::vector<ExecuteServiceArgument> args{}; | ||||
|   FixedVector<ExecuteServiceArgument> args{}; | ||||
|   void decode(const uint8_t *buffer, size_t length) override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -1369,27 +1371,28 @@ class CameraImageRequest final : public ProtoDecodableMessage { | ||||
| class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 46; | ||||
|   static constexpr uint8_t ESTIMATED_SIZE = 145; | ||||
|   static constexpr uint8_t ESTIMATED_SIZE = 150; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_climate_response"; } | ||||
| #endif | ||||
|   bool supports_current_temperature{false}; | ||||
|   bool supports_two_point_target_temperature{false}; | ||||
|   const std::set<climate::ClimateMode> *supported_modes{}; | ||||
|   const climate::ClimateModeMask *supported_modes{}; | ||||
|   float visual_min_temperature{0.0f}; | ||||
|   float visual_max_temperature{0.0f}; | ||||
|   float visual_target_temperature_step{0.0f}; | ||||
|   bool supports_action{false}; | ||||
|   const std::set<climate::ClimateFanMode> *supported_fan_modes{}; | ||||
|   const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; | ||||
|   const std::set<std::string> *supported_custom_fan_modes{}; | ||||
|   const std::set<climate::ClimatePreset> *supported_presets{}; | ||||
|   const std::set<std::string> *supported_custom_presets{}; | ||||
|   const climate::ClimateFanModeMask *supported_fan_modes{}; | ||||
|   const climate::ClimateSwingModeMask *supported_swing_modes{}; | ||||
|   const std::vector<std::string> *supported_custom_fan_modes{}; | ||||
|   const climate::ClimatePresetMask *supported_presets{}; | ||||
|   const std::vector<std::string> *supported_custom_presets{}; | ||||
|   float visual_current_temperature_step{0.0f}; | ||||
|   bool supports_current_humidity{false}; | ||||
|   bool supports_target_humidity{false}; | ||||
|   float visual_min_humidity{0.0f}; | ||||
|   float visual_max_humidity{0.0f}; | ||||
|   uint32_t feature_flags{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1531,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_select_response"; } | ||||
| #endif | ||||
|   const std::vector<std::string> *options{}; | ||||
|   const FixedVector<const char *> *options{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1923,7 +1926,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { | ||||
|   std::array<uint64_t, 2> uuid{}; | ||||
|   uint32_t handle{0}; | ||||
|   uint32_t properties{0}; | ||||
|   std::vector<BluetoothGATTDescriptor> descriptors{}; | ||||
|   FixedVector<BluetoothGATTDescriptor> descriptors{}; | ||||
|   uint32_t short_uuid{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| @@ -1937,7 +1940,7 @@ class BluetoothGATTService final : public ProtoMessage { | ||||
|  public: | ||||
|   std::array<uint64_t, 2> uuid{}; | ||||
|   uint32_t handle{0}; | ||||
|   std::vector<BluetoothGATTCharacteristic> characteristics{}; | ||||
|   FixedVector<BluetoothGATTCharacteristic> characteristics{}; | ||||
|   uint32_t short_uuid{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
|   | ||||
| @@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value | ||||
|   out.append("\n"); | ||||
| } | ||||
|  | ||||
| static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { | ||||
|   append_field_prefix(out, field_name, indent); | ||||
|   out.append("'").append(value).append("'"); | ||||
|   out.append("\n"); | ||||
| } | ||||
|  | ||||
| template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { | ||||
|   append_field_prefix(out, field_name, indent); | ||||
|   out.append(proto_enum_to_string<T>(value)); | ||||
| @@ -1292,6 +1298,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||
| #ifdef USE_DEVICES | ||||
|   dump_field(out, "device_id", this->device_id); | ||||
| #endif | ||||
|   dump_field(out, "feature_flags", this->feature_flags); | ||||
| } | ||||
| void ClimateStateResponse::dump_to(std::string &out) const { | ||||
|   MessageDumpHelper helper(out, "ClimateStateResponse"); | ||||
|   | ||||
| @@ -468,6 +468,31 @@ uint16_t APIServer::get_port() const { return this->port_; } | ||||
| void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, | ||||
|                                   const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { | ||||
|   if (!this->noise_pref_.save(&new_psk)) { | ||||
|     ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); | ||||
|     return false; | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, active_psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(active_psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
| @@ -482,27 +507,21 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
|   } | ||||
|  | ||||
|   SavedNoisePsk new_saved_psk{psk}; | ||||
|   if (!this->noise_pref_.save(&new_saved_psk)) { | ||||
|     ESP_LOGW(TAG, "Failed to save Noise PSK"); | ||||
|     return false; | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Noise PSK saved"); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
|   return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, | ||||
|                                  make_active); | ||||
| #endif | ||||
| } | ||||
| bool APIServer::clear_noise_psk(bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
|   // but if it is, reject the change | ||||
|   ESP_LOGW(TAG, "Key set in YAML"); | ||||
|   return false; | ||||
| #else | ||||
|   SavedNoisePsk empty_psk{}; | ||||
|   psk_t empty{}; | ||||
|   return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, | ||||
|                                  make_active); | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|   | ||||
| @@ -53,6 +53,7 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
|   bool save_noise_psk(psk_t psk, bool make_active = true); | ||||
|   bool clear_noise_psk(bool make_active = true); | ||||
|   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } | ||||
|   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } | ||||
| #endif  // USE_API_NOISE | ||||
| @@ -124,8 +125,14 @@ class APIServer : public Component, public Controller { | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
| #ifdef USE_API_SERVICES | ||||
|   void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) { | ||||
|     this->user_services_.assign(services); | ||||
|   } | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|   // Only compile push_back method when custom_services: true (external components) | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
| #endif | ||||
| @@ -174,6 +181,10 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
|  protected: | ||||
|   void schedule_reboot_timeout_(); | ||||
| #ifdef USE_API_NOISE | ||||
|   bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, | ||||
|                          const psk_t &active_psk, bool make_active); | ||||
| #endif  // USE_API_NOISE | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   | ||||
| @@ -53,8 +53,14 @@ class CustomAPIDevice { | ||||
|   template<typename T, typename... Ts> | ||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T, typename... Ts> | ||||
| @@ -86,8 +92,14 @@ class CustomAPIDevice { | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
| @@ -201,9 +213,9 @@ class CustomAPIDevice { | ||||
|   void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { | ||||
|     HomeassistantActionRequest resp; | ||||
|     resp.set_service(StringRef(service_name)); | ||||
|     resp.data.init(data.size()); | ||||
|     for (auto &it : data) { | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       auto &kv = resp.data.emplace_back(); | ||||
|       kv.set_key(StringRef(it.first)); | ||||
|       kv.value = it.second; | ||||
|     } | ||||
| @@ -244,9 +256,9 @@ class CustomAPIDevice { | ||||
|     HomeassistantActionRequest resp; | ||||
|     resp.set_service(StringRef(service_name)); | ||||
|     resp.is_event = true; | ||||
|     resp.data.init(data.size()); | ||||
|     for (auto &it : data) { | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       auto &kv = resp.data.emplace_back(); | ||||
|       kv.set_key(StringRef(it.first)); | ||||
|       kv.value = it.second; | ||||
|     } | ||||
|   | ||||
| @@ -41,10 +41,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s | ||||
|  | ||||
| template<typename... Ts> class TemplatableKeyValuePair { | ||||
|  public: | ||||
|   // Default constructor needed for FixedVector::emplace_back() | ||||
|   TemplatableKeyValuePair() = default; | ||||
|  | ||||
|   // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") | ||||
|   // and never templatable values or lambdas. Only the value parameter can be a lambda/template. | ||||
|   // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. | ||||
|   template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} | ||||
|  | ||||
|   std::string key; | ||||
|   TemplatableStringValue<Ts...> value; | ||||
| }; | ||||
| @@ -93,15 +97,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|  | ||||
|   template<typename T> void set_service(T service) { this->service_ = service; } | ||||
|  | ||||
|   // Initialize FixedVector members - called from Python codegen with compile-time known sizes. | ||||
|   // Must be called before any add_* methods; capacity must match the number of subsequent add_* calls. | ||||
|   void init_data(size_t count) { this->data_.init(count); } | ||||
|   void init_data_template(size_t count) { this->data_template_.init(count); } | ||||
|   void init_variables(size_t count) { this->variables_.init(count); } | ||||
|  | ||||
|   // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). | ||||
|   // The value parameter can be a lambda/template, but keys are never templatable. | ||||
|   // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues. | ||||
|   template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); } | ||||
|   template<typename T> void add_data_template(std::string key, T value) { | ||||
|     this->data_template_.emplace_back(std::move(key), value); | ||||
|   template<typename K, typename V> void add_data(K &&key, V &&value) { | ||||
|     this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value)); | ||||
|   } | ||||
|   template<typename T> void add_variable(std::string key, T value) { | ||||
|     this->variables_.emplace_back(std::move(key), value); | ||||
|   template<typename K, typename V> void add_data_template(K &&key, V &&value) { | ||||
|     this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value)); | ||||
|   } | ||||
|   template<typename K, typename V> void add_variable(K &&key, V &&value) { | ||||
|     this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value)); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| @@ -127,24 +138,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|     std::string service_value = this->service_.value(x...); | ||||
|     resp.set_service(StringRef(service_value)); | ||||
|     resp.is_event = this->flags_.is_event; | ||||
|     for (auto &it : this->data_) { | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|     } | ||||
|     for (auto &it : this->data_template_) { | ||||
|       resp.data_template.emplace_back(); | ||||
|       auto &kv = resp.data_template.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|     } | ||||
|     for (auto &it : this->variables_) { | ||||
|       resp.variables.emplace_back(); | ||||
|       auto &kv = resp.variables.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|     } | ||||
|     this->populate_service_map(resp.data, this->data_, x...); | ||||
|     this->populate_service_map(resp.data_template, this->data_template_, x...); | ||||
|     this->populate_service_map(resp.variables, this->variables_, x...); | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|     if (this->flags_.wants_status) { | ||||
| @@ -189,11 +185,28 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   // Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies | ||||
|   template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) { | ||||
|     auto &kv = vec.emplace_back(); | ||||
|     kv.key = std::forward<K>(key); | ||||
|     kv.value = std::forward<V>(value); | ||||
|   } | ||||
|  | ||||
|   template<typename VectorType, typename SourceType> | ||||
|   static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) { | ||||
|     dest.init(source.size()); | ||||
|     for (auto &it : source) { | ||||
|       auto &kv = dest.emplace_back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   APIServer *parent_; | ||||
|   TemplatableStringValue<Ts...> service_{}; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_template_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> variables_; | ||||
|   FixedVector<TemplatableKeyValuePair<Ts...>> data_; | ||||
|   FixedVector<TemplatableKeyValuePair<Ts...>> data_template_; | ||||
|   FixedVector<TemplatableKeyValuePair<Ts...>> variables_; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   TemplatableStringValue<Ts...> response_template_{""}; | ||||
|   | ||||
| @@ -7,6 +7,69 @@ namespace esphome::api { | ||||
|  | ||||
| static const char *const TAG = "api.proto"; | ||||
|  | ||||
| uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) { | ||||
|   uint32_t count = 0; | ||||
|   const uint8_t *ptr = buffer; | ||||
|   const uint8_t *end = buffer + length; | ||||
|  | ||||
|   while (ptr < end) { | ||||
|     uint32_t consumed; | ||||
|  | ||||
|     // Parse field header (tag) | ||||
|     auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|     if (!res.has_value()) { | ||||
|       break;  // Invalid data, stop counting | ||||
|     } | ||||
|  | ||||
|     uint32_t tag = res->as_uint32(); | ||||
|     uint32_t field_type = tag & WIRE_TYPE_MASK; | ||||
|     uint32_t field_id = tag >> 3; | ||||
|     ptr += consumed; | ||||
|  | ||||
|     // Count if this is the target field | ||||
|     if (field_id == target_field_id) { | ||||
|       count++; | ||||
|     } | ||||
|  | ||||
|     // Skip field data based on wire type | ||||
|     switch (field_type) { | ||||
|       case WIRE_TYPE_VARINT: {  // VarInt - parse and skip | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           return count;  // Invalid data, return what we have | ||||
|         } | ||||
|         ptr += consumed; | ||||
|         break; | ||||
|       } | ||||
|       case WIRE_TYPE_LENGTH_DELIMITED: {  // Length-delimited - parse length and skip data | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           return count; | ||||
|         } | ||||
|         uint32_t field_length = res->as_uint32(); | ||||
|         ptr += consumed; | ||||
|         if (ptr + field_length > end) { | ||||
|           return count;  // Out of bounds | ||||
|         } | ||||
|         ptr += field_length; | ||||
|         break; | ||||
|       } | ||||
|       case WIRE_TYPE_FIXED32: {  // 32-bit - skip 4 bytes | ||||
|         if (ptr + 4 > end) { | ||||
|           return count; | ||||
|         } | ||||
|         ptr += 4; | ||||
|         break; | ||||
|       } | ||||
|       default: | ||||
|         // Unknown wire type, can't continue | ||||
|         return count; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return count; | ||||
| } | ||||
|  | ||||
| void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|   const uint8_t *ptr = buffer; | ||||
|   const uint8_t *end = buffer + length; | ||||
| @@ -22,12 +85,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|     } | ||||
|  | ||||
|     uint32_t tag = res->as_uint32(); | ||||
|     uint32_t field_type = tag & 0b111; | ||||
|     uint32_t field_type = tag & WIRE_TYPE_MASK; | ||||
|     uint32_t field_id = tag >> 3; | ||||
|     ptr += consumed; | ||||
|  | ||||
|     switch (field_type) { | ||||
|       case 0: {  // VarInt | ||||
|       case WIRE_TYPE_VARINT: {  // VarInt | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); | ||||
| @@ -39,7 +102,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|         ptr += consumed; | ||||
|         break; | ||||
|       } | ||||
|       case 2: {  // Length-delimited | ||||
|       case WIRE_TYPE_LENGTH_DELIMITED: {  // Length-delimited | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); | ||||
| @@ -57,7 +120,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|         ptr += field_length; | ||||
|         break; | ||||
|       } | ||||
|       case 5: {  // 32-bit | ||||
|       case WIRE_TYPE_FIXED32: {  // 32-bit | ||||
|         if (ptr + 4 > end) { | ||||
|           ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); | ||||
|           return; | ||||
|   | ||||
| @@ -15,6 +15,13 @@ | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| // Protocol Buffer wire type constants | ||||
| // See https://protobuf.dev/programming-guides/encoding/#structure | ||||
| constexpr uint8_t WIRE_TYPE_VARINT = 0;            // int32, int64, uint32, uint64, sint32, sint64, bool, enum | ||||
| constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2;  // string, bytes, embedded messages, packed repeated fields | ||||
| constexpr uint8_t WIRE_TYPE_FIXED32 = 5;           // fixed32, sfixed32, float | ||||
| constexpr uint8_t WIRE_TYPE_MASK = 0b111;          // Mask to extract wire type from tag | ||||
|  | ||||
| // Helper functions for ZigZag encoding/decoding | ||||
| inline constexpr uint32_t encode_zigzag32(int32_t value) { | ||||
|   return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); | ||||
| @@ -241,7 +248,7 @@ class ProtoWriteBuffer { | ||||
|    * Following https://protobuf.dev/programming-guides/encoding/#structure | ||||
|    */ | ||||
|   void encode_field_raw(uint32_t field_id, uint32_t type) { | ||||
|     uint32_t val = (field_id << 3) | (type & 0b111); | ||||
|     uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); | ||||
|     this->encode_varint_raw(val); | ||||
|   } | ||||
|   void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { | ||||
| @@ -354,7 +361,18 @@ class ProtoMessage { | ||||
| // Base class for messages that support decoding | ||||
| class ProtoDecodableMessage : public ProtoMessage { | ||||
|  public: | ||||
|   void decode(const uint8_t *buffer, size_t length); | ||||
|   virtual void decode(const uint8_t *buffer, size_t length); | ||||
|  | ||||
|   /** | ||||
|    * Count occurrences of a repeated field in a protobuf buffer. | ||||
|    * This is a lightweight scan that only parses tags and skips field data. | ||||
|    * | ||||
|    * @param buffer Pointer to the protobuf buffer | ||||
|    * @param length Length of the buffer in bytes | ||||
|    * @param target_field_id The field ID to count | ||||
|    * @return Number of times the field appears in the buffer | ||||
|    */ | ||||
|   static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); | ||||
|  | ||||
|  protected: | ||||
|   virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } | ||||
| @@ -482,7 +500,7 @@ class ProtoSize { | ||||
|    * @return The number of bytes needed to encode the field ID and wire type | ||||
|    */ | ||||
|   static constexpr uint32_t field(uint32_t field_id, uint32_t type) { | ||||
|     uint32_t tag = (field_id << 3) | (type & 0b111); | ||||
|     uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK); | ||||
|     return varint(tag); | ||||
|   } | ||||
|  | ||||
| @@ -749,13 +767,29 @@ class ProtoSize { | ||||
|   template<typename MessageType> | ||||
|   inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) { | ||||
|     // Skip if the vector is empty | ||||
|     if (messages.empty()) { | ||||
|       return; | ||||
|     if (!messages.empty()) { | ||||
|       // Use the force version for all messages in the repeated field | ||||
|       for (const auto &message : messages) { | ||||
|         add_message_object_force(field_id_size, message); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     // Use the force version for all messages in the repeated field | ||||
|     for (const auto &message : messages) { | ||||
|       add_message_object_force(field_id_size, message); | ||||
|   /** | ||||
|    * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector | ||||
|    * version) | ||||
|    * | ||||
|    * @tparam MessageType The type of the nested messages in the FixedVector | ||||
|    * @param messages FixedVector of message objects | ||||
|    */ | ||||
|   template<typename MessageType> | ||||
|   inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) { | ||||
|     // Skip if the fixed vector is empty | ||||
|     if (!messages.empty()) { | ||||
|       // Use the force version for all messages in the repeated field | ||||
|       for (const auto &message : messages) { | ||||
|         add_message_object_force(field_id_size, message); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -11,16 +11,49 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument & | ||||
| } | ||||
| template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } | ||||
| template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve | ||||
| template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.bool_array; | ||||
|   std::vector<bool> result; | ||||
|   result.reserve(arg.bool_array.size()); | ||||
|   result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.int_array; | ||||
|   std::vector<int32_t> result; | ||||
|   result.reserve(arg.int_array.size()); | ||||
|   result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.float_array; | ||||
|   std::vector<float> result; | ||||
|   result.reserve(arg.float_array.size()); | ||||
|   result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { | ||||
|   std::vector<std::string> result; | ||||
|   result.reserve(arg.string_array.size()); | ||||
|   result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services - zero-copy | ||||
| template<> | ||||
| const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.bool_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.int_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.float_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>( | ||||
|     const ExecuteServiceArgument &arg) { | ||||
|   return arg.string_array; | ||||
| } | ||||
|  | ||||
| @@ -28,6 +61,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER | ||||
| template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -35,9 +35,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { | ||||
|     msg.set_name(StringRef(this->name_)); | ||||
|     msg.key = this->key_; | ||||
|     std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; | ||||
|     msg.args.init(sizeof...(Ts)); | ||||
|     for (size_t i = 0; i < sizeof...(Ts); i++) { | ||||
|       msg.args.emplace_back(); | ||||
|       auto &arg = msg.args.back(); | ||||
|       auto &arg = msg.args.emplace_back(); | ||||
|       arg.type = arg_types[i]; | ||||
|       arg.set_name(StringRef(this->arg_names_[i])); | ||||
|     } | ||||
| @@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { | ||||
|  | ||||
|  protected: | ||||
|   virtual void execute(Ts... x) = 0; | ||||
|   template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) { | ||||
|   template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) { | ||||
|     this->execute((get_execute_arg_value<Ts>(args[S]))...); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,9 @@ namespace bang_bang { | ||||
|  | ||||
| static const char *const TAG = "bang_bang.climate"; | ||||
|  | ||||
| BangBangClimate::BangBangClimate() | ||||
|     : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void BangBangClimate::setup() { | ||||
|   this->sensor_->add_on_state_callback([this](float state) { | ||||
|     this->current_temperature = state; | ||||
| @@ -31,53 +34,63 @@ void BangBangClimate::setup() { | ||||
|     restore->to_call(this).perform(); | ||||
|   } else { | ||||
|     // restore from defaults, change_away handles those for us | ||||
|     if (supports_cool_ && supports_heat_) { | ||||
|     if (this->supports_cool_ && this->supports_heat_) { | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||
|     } else if (supports_cool_) { | ||||
|     } else if (this->supports_cool_) { | ||||
|       this->mode = climate::CLIMATE_MODE_COOL; | ||||
|     } else if (supports_heat_) { | ||||
|     } else if (this->supports_heat_) { | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|     } | ||||
|     this->change_away_(false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BangBangClimate::control(const climate::ClimateCall &call) { | ||||
|   if (call.get_mode().has_value()) | ||||
|   if (call.get_mode().has_value()) { | ||||
|     this->mode = *call.get_mode(); | ||||
|   if (call.get_target_temperature_low().has_value()) | ||||
|   } | ||||
|   if (call.get_target_temperature_low().has_value()) { | ||||
|     this->target_temperature_low = *call.get_target_temperature_low(); | ||||
|   if (call.get_target_temperature_high().has_value()) | ||||
|   } | ||||
|   if (call.get_target_temperature_high().has_value()) { | ||||
|     this->target_temperature_high = *call.get_target_temperature_high(); | ||||
|   if (call.get_preset().has_value()) | ||||
|   } | ||||
|   if (call.get_preset().has_value()) { | ||||
|     this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); | ||||
|   } | ||||
|  | ||||
|   this->compute_state_(); | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| climate::ClimateTraits BangBangClimate::traits() { | ||||
|   auto traits = climate::ClimateTraits(); | ||||
|   traits.set_supports_current_temperature(true); | ||||
|   if (this->humidity_sensor_ != nullptr) | ||||
|     traits.set_supports_current_humidity(true); | ||||
|   traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | | ||||
|                            climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); | ||||
|   if (this->humidity_sensor_ != nullptr) { | ||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); | ||||
|   } | ||||
|   traits.set_supported_modes({ | ||||
|       climate::CLIMATE_MODE_OFF, | ||||
|   }); | ||||
|   if (supports_cool_) | ||||
|   if (this->supports_cool_) { | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||
|   if (supports_heat_) | ||||
|   } | ||||
|   if (this->supports_heat_) { | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); | ||||
|   if (supports_cool_ && supports_heat_) | ||||
|   } | ||||
|   if (this->supports_cool_ && this->supports_heat_) { | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); | ||||
|   traits.set_supports_two_point_target_temperature(true); | ||||
|   if (supports_away_) { | ||||
|   } | ||||
|   if (this->supports_away_) { | ||||
|     traits.set_supported_presets({ | ||||
|         climate::CLIMATE_PRESET_HOME, | ||||
|         climate::CLIMATE_PRESET_AWAY, | ||||
|     }); | ||||
|   } | ||||
|   traits.set_supports_action(true); | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| void BangBangClimate::compute_state_() { | ||||
|   if (this->mode == climate::CLIMATE_MODE_OFF) { | ||||
|     this->switch_to_action_(climate::CLIMATE_ACTION_OFF); | ||||
| @@ -122,6 +135,7 @@ void BangBangClimate::compute_state_() { | ||||
|  | ||||
|   this->switch_to_action_(target_action); | ||||
| } | ||||
|  | ||||
| void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | ||||
|   if (action == this->action) { | ||||
|     // already in target mode | ||||
| @@ -166,6 +180,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | ||||
|   this->prev_trigger_ = trig; | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void BangBangClimate::change_away_(bool away) { | ||||
|   if (!away) { | ||||
|     this->target_temperature_low = this->normal_config_.default_temperature_low; | ||||
| @@ -176,22 +191,26 @@ void BangBangClimate::change_away_(bool away) { | ||||
|   } | ||||
|   this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; | ||||
| } | ||||
|  | ||||
| void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { | ||||
|   this->normal_config_ = normal_config; | ||||
| } | ||||
|  | ||||
| void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) { | ||||
|   this->supports_away_ = true; | ||||
|   this->away_config_ = away_config; | ||||
| } | ||||
| BangBangClimate::BangBangClimate() | ||||
|     : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | ||||
| void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } | ||||
|  | ||||
| Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } | ||||
| Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } | ||||
| void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } | ||||
| Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } | ||||
|  | ||||
| void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } | ||||
| void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } | ||||
|  | ||||
| void BangBangClimate::dump_config() { | ||||
|   LOG_CLIMATE("", "Bang Bang Climate", this); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|   | ||||
| @@ -25,14 +25,15 @@ class BangBangClimate : public climate::Climate, public Component { | ||||
|  | ||||
|   void set_sensor(sensor::Sensor *sensor); | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity_sensor); | ||||
|   Trigger<> *get_idle_trigger() const; | ||||
|   Trigger<> *get_cool_trigger() const; | ||||
|   void set_supports_cool(bool supports_cool); | ||||
|   Trigger<> *get_heat_trigger() const; | ||||
|   void set_supports_heat(bool supports_heat); | ||||
|   void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); | ||||
|   void set_away_config(const BangBangClimateTargetTempConfig &away_config); | ||||
|  | ||||
|   Trigger<> *get_idle_trigger() const; | ||||
|   Trigger<> *get_cool_trigger() const; | ||||
|   Trigger<> *get_heat_trigger() const; | ||||
|  | ||||
|  protected: | ||||
|   /// Override control to change settings of the climate device. | ||||
|   void control(const climate::ClimateCall &call) override; | ||||
| @@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component { | ||||
|    * | ||||
|    * In idle mode, the controller is assumed to have both heating and cooling disabled. | ||||
|    */ | ||||
|   Trigger<> *idle_trigger_; | ||||
|   Trigger<> *idle_trigger_{nullptr}; | ||||
|   /** The trigger to call when the controller should switch to cooling mode. | ||||
|    */ | ||||
|   Trigger<> *cool_trigger_; | ||||
|   /** Whether the controller supports cooling. | ||||
|    * | ||||
|    * A false value for this attribute means that the controller has no cooling action | ||||
|    * (for example a thermostat, where only heating and not-heating is possible). | ||||
|    */ | ||||
|   bool supports_cool_{false}; | ||||
|   Trigger<> *cool_trigger_{nullptr}; | ||||
|   /** The trigger to call when the controller should switch to heating mode. | ||||
|    * | ||||
|    * A null value for this attribute means that the controller has no heating action | ||||
| @@ -73,15 +68,23 @@ class BangBangClimate : public climate::Climate, public Component { | ||||
|    * (blinds open) is possible. | ||||
|    */ | ||||
|   Trigger<> *heat_trigger_{nullptr}; | ||||
|   bool supports_heat_{false}; | ||||
|   /** A reference to the trigger that was previously active. | ||||
|    * | ||||
|    * This is so that the previous trigger can be stopped before enabling a new one. | ||||
|    */ | ||||
|   Trigger<> *prev_trigger_{nullptr}; | ||||
|  | ||||
|   BangBangClimateTargetTempConfig normal_config_{}; | ||||
|   /** Whether the controller supports cooling/heating | ||||
|    * | ||||
|    * A false value for this attribute means that the controller has no respective action | ||||
|    * (for example a thermostat, where only heating and not-heating is possible). | ||||
|    */ | ||||
|   bool supports_cool_{false}; | ||||
|   bool supports_heat_{false}; | ||||
|  | ||||
|   bool supports_away_{false}; | ||||
|  | ||||
|   BangBangClimateTargetTempConfig normal_config_{}; | ||||
|   BangBangClimateTargetTempConfig away_config_{}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t { | ||||
|  | ||||
| static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -33,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | ||||
|  | ||||
|   climate::ClimateTraits traits() override { | ||||
|     auto traits = climate::ClimateTraits(); | ||||
|     traits.set_supports_action(true); | ||||
|     traits.set_supports_current_temperature(true); | ||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|     traits.set_supported_modes({ | ||||
|         climate::CLIMATE_MODE_OFF, | ||||
|         climate::CLIMATE_MODE_HEAT, | ||||
| @@ -44,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | ||||
|     }); | ||||
|  | ||||
|     // 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_custom_fan_modes(BEDJET_FAN_STEP_NAMES); | ||||
|     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, | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/bh1900nux/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/bh1900nux/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								esphome/components/bh1900nux/bh1900nux.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								esphome/components/bh1900nux/bh1900nux.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "bh1900nux.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bh1900nux { | ||||
|  | ||||
| static const char *const TAG = "bh1900nux.sensor"; | ||||
|  | ||||
| // I2C Registers | ||||
| static const uint8_t TEMPERATURE_REG = 0x00; | ||||
| static const uint8_t CONFIG_REG = 0x01;            // Not used and supported yet | ||||
| static const uint8_t TEMPERATURE_LOW_REG = 0x02;   // Not used and supported yet | ||||
| static const uint8_t TEMPERATURE_HIGH_REG = 0x03;  // Not used and supported yet | ||||
| static const uint8_t SOFT_RESET_REG = 0x04; | ||||
|  | ||||
| // I2C Command payloads | ||||
| static const uint8_t SOFT_RESET_PAYLOAD = 0x01;  // Soft Reset value | ||||
|  | ||||
| static const float SENSOR_RESOLUTION = 0.0625f;  // Sensor resolution per bit in degrees celsius | ||||
|  | ||||
| void BH1900NUXSensor::setup() { | ||||
|   // Initialize I2C device | ||||
|   i2c::ErrorCode result_code = | ||||
|       this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1);  // Software Reset to check communication | ||||
|   if (result_code != i2c::ERROR_OK) { | ||||
|     this->mark_failed(ESP_LOG_MSG_COMM_FAIL); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BH1900NUXSensor::update() { | ||||
|   uint8_t temperature_raw[2]; | ||||
|   if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Combined raw value, unsigned and unaligned 16 bit | ||||
|   // Temperature is represented in just 12 bits, shift needed | ||||
|   int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]); | ||||
|   raw_temperature_register_value >>= 4; | ||||
|   float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION;  // Apply sensor resolution | ||||
|  | ||||
|   this->publish_state(temperature_value); | ||||
| } | ||||
|  | ||||
| void BH1900NUXSensor::dump_config() { | ||||
|   LOG_SENSOR("", "BH1900NUX", this); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| }  // namespace bh1900nux | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/bh1900nux/bh1900nux.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/bh1900nux/bh1900nux.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bh1900nux { | ||||
|  | ||||
| class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace bh1900nux | ||||
| }  // namespace esphome | ||||
							
								
								
									
										34
									
								
								esphome/components/bh1900nux/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/bh1900nux/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| CODEOWNERS = ["@B48D81EFCC"] | ||||
|  | ||||
| sensor_ns = cg.esphome_ns.namespace("bh1900nux") | ||||
| BH1900NUXSensor = sensor_ns.class_( | ||||
|     "BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         BH1900NUXSensor, | ||||
|         accuracy_decimals=1, | ||||
|         unit_of_measurement=UNIT_CELSIUS, | ||||
|         device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x48)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
| @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon | ||||
| InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) | ||||
| AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) | ||||
| LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) | ||||
|  | ||||
| _LOGGER = getLogger(__name__) | ||||
| @@ -264,20 +265,31 @@ async def delayed_off_filter_to_code(config, filter_id): | ||||
|     ), | ||||
| ) | ||||
| async def autorepeat_filter_to_code(config, filter_id): | ||||
|     timings = [] | ||||
|     if len(config) > 0: | ||||
|         timings.extend( | ||||
|             (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) | ||||
|             for conf in config | ||||
|         ) | ||||
|     else: | ||||
|         timings.append( | ||||
|             ( | ||||
|                 cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, | ||||
|                 cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, | ||||
|                 cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, | ||||
|         timings = [ | ||||
|             cg.StructInitializer( | ||||
|                 cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), | ||||
|                 ("delay", conf[CONF_DELAY]), | ||||
|                 ("time_off", conf[CONF_TIME_OFF]), | ||||
|                 ("time_on", conf[CONF_TIME_ON]), | ||||
|             ) | ||||
|         ) | ||||
|             for conf in config | ||||
|         ] | ||||
|     else: | ||||
|         timings = [ | ||||
|             cg.StructInitializer( | ||||
|                 cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), | ||||
|                 ("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds), | ||||
|                 ( | ||||
|                     "time_off", | ||||
|                     cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, | ||||
|                 ), | ||||
|                 ( | ||||
|                     "time_on", | ||||
|                     cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, | ||||
|                 ), | ||||
|             ) | ||||
|         ] | ||||
|     var = cg.new_Pvariable(filter_id, timings) | ||||
|     await cg.register_component(var, {}) | ||||
|     return var | ||||
| @@ -288,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(bool, "x")], return_type=cg.optional.template(bool) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
| @@ -536,11 +548,6 @@ def binary_sensor_schema( | ||||
|     return _BINARY_SENSOR_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| BINARY_SENSOR_SCHEMA = binary_sensor_schema() | ||||
| BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) | ||||
|  | ||||
|  | ||||
| async def setup_binary_sensor_core_(var, config): | ||||
|     await setup_entity(var, config, "binary_sensor") | ||||
|  | ||||
|   | ||||
| @@ -2,11 +2,11 @@ | ||||
|  | ||||
| #include <cinttypes> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> { | ||||
|  | ||||
| class MultiClickTrigger : public Trigger<>, public Component { | ||||
|  public: | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(std::move(timing)) {} | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(timing) {} | ||||
|  | ||||
|   void setup() override { | ||||
|     this->last_state_ = this->parent_->get_state_default(false); | ||||
| @@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component { | ||||
|   void trigger_(); | ||||
|  | ||||
|   BinarySensor *parent_; | ||||
|   std::vector<MultiClickTriggerEvent> timing_; | ||||
|   FixedVector<MultiClickTriggerEvent> timing_; | ||||
|   uint32_t invalid_cooldown_{1000}; | ||||
|   optional<size_t> at_index_{}; | ||||
|   bool last_state_{false}; | ||||
|   | ||||
| @@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) { | ||||
|     last_filter->next_ = filter; | ||||
|   } | ||||
| } | ||||
| void BinarySensor::add_filters(const std::vector<Filter *> &filters) { | ||||
| void BinarySensor::add_filters(std::initializer_list<Filter *> filters) { | ||||
|   for (Filter *filter : filters) { | ||||
|     this->add_filter(filter); | ||||
|   } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/filter.h" | ||||
|  | ||||
| #include <vector> | ||||
| #include <initializer_list> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| @@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl | ||||
|   void publish_initial_state(bool new_state); | ||||
|  | ||||
|   void add_filter(Filter *filter); | ||||
|   void add_filters(const std::vector<Filter *> &filters); | ||||
|   void add_filters(std::initializer_list<Filter *> filters); | ||||
|  | ||||
|   // ========== INTERNAL METHODS ========== | ||||
|   // (In most use cases you won't need these) | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| #include "filter.h" | ||||
|  | ||||
| #include "binary_sensor.h" | ||||
| #include <utility> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| @@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD | ||||
|  | ||||
| optional<bool> InvertFilter::new_value(bool value) { return !value; } | ||||
|  | ||||
| AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} | ||||
| AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {} | ||||
|  | ||||
| optional<bool> AutorepeatFilter::new_value(bool value) { | ||||
|   if (value) { | ||||
|   | ||||
| @@ -4,8 +4,6 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| namespace binary_sensor { | ||||
| @@ -82,11 +80,6 @@ class InvertFilter : public Filter { | ||||
| }; | ||||
|  | ||||
| struct AutorepeatFilterTiming { | ||||
|   AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { | ||||
|     this->delay = delay; | ||||
|     this->time_off = off; | ||||
|     this->time_on = on; | ||||
|   } | ||||
|   uint32_t delay; | ||||
|   uint32_t time_off; | ||||
|   uint32_t time_on; | ||||
| @@ -94,7 +87,7 @@ struct AutorepeatFilterTiming { | ||||
|  | ||||
| class AutorepeatFilter : public Filter, public Component { | ||||
|  public: | ||||
|   explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); | ||||
|   explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings); | ||||
|  | ||||
|   optional<bool> new_value(bool value) override; | ||||
|  | ||||
| @@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component { | ||||
|   void next_timing_(); | ||||
|   void next_value_(bool val); | ||||
|  | ||||
|   std::vector<AutorepeatFilterTiming> timings_; | ||||
|   FixedVector<AutorepeatFilterTiming> timings_; | ||||
|   uint8_t active_timing_{0}; | ||||
| }; | ||||
|  | ||||
| @@ -118,6 +111,21 @@ class LambdaFilter : public Filter { | ||||
|   std::function<optional<bool>(bool)> f_; | ||||
| }; | ||||
|  | ||||
| /** Optimized lambda filter for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessLambdaFilter : public Filter { | ||||
|  public: | ||||
|   explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {} | ||||
|  | ||||
|   optional<bool> new_value(bool value) override { return this->f_(value); } | ||||
|  | ||||
|  protected: | ||||
|   optional<bool> (*f_)(bool); | ||||
| }; | ||||
|  | ||||
| class SettleFilter : public Filter, public Component { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override; | ||||
|   | ||||
| @@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   BLEClientWriteAction(BLEClient *ble_client) { | ||||
|     ble_client->register_ble_node(this); | ||||
|     ble_client_ = ble_client; | ||||
|     this->construct_simple_value_(); | ||||
|   } | ||||
|  | ||||
|   ~BLEClientWriteAction() { this->destroy_simple_value_(); } | ||||
|  | ||||
|   void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } | ||||
|   void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
| @@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|  | ||||
|   void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   void set_value_template(std::vector<uint8_t> (*func)(Ts...)) { | ||||
|     this->destroy_simple_value_(); | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const std::vector<uint8_t> &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|     if (!this->has_simple_value_) { | ||||
|       this->construct_simple_value_(); | ||||
|     } | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|   void play(Ts... x) override {} | ||||
| @@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   void play_complex(Ts... x) override { | ||||
|     this->num_running_++; | ||||
|     this->var_ = std::make_tuple(x...); | ||||
|     auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); | ||||
|     auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); | ||||
|     // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. | ||||
|     if (!write(value)) | ||||
|       this->play_next_(x...); | ||||
| @@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); } | ||||
|  | ||||
|   void destroy_simple_value_() { | ||||
|     if (this->has_simple_value_) { | ||||
|       this->value_.simple.~vector(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   BLEClient *ble_client_; | ||||
|   bool has_simple_value_ = true; | ||||
|   std::vector<uint8_t> value_simple_; | ||||
|   std::function<std::vector<uint8_t>(Ts...)> value_template_{}; | ||||
|   union Value { | ||||
|     std::vector<uint8_t> simple; | ||||
|     std::vector<uint8_t> (*template_func)(Ts...); | ||||
|     Value() {}   // trivial constructor | ||||
|     ~Value() {}  // trivial destructor - we manage lifetime via discriminator | ||||
|   } value_; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   std::tuple<Ts...> var_{}; | ||||
| @@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts... | ||||
|   void play(Ts... x) override { | ||||
|     uint32_t passkey; | ||||
|     if (has_simple_value_) { | ||||
|       passkey = this->value_simple_; | ||||
|       passkey = this->value_.simple; | ||||
|     } else { | ||||
|       passkey = this->value_template_(x...); | ||||
|       passkey = this->value_.template_func(x...); | ||||
|     } | ||||
|     if (passkey > 999999) | ||||
|       return; | ||||
| @@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts... | ||||
|     esp_ble_passkey_reply(remote_bda, true, passkey); | ||||
|   } | ||||
|  | ||||
|   void set_value_template(std::function<uint32_t(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   void set_value_template(uint32_t (*func)(Ts...)) { | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const uint32_t &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   uint32_t value_simple_{0}; | ||||
|   std::function<uint32_t(Ts...)> value_template_{}; | ||||
|   union { | ||||
|     uint32_t simple; | ||||
|     uint32_t (*template_func)(Ts...); | ||||
|   } value_{.simple = 0}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { | ||||
| @@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac | ||||
|     esp_bd_addr_t remote_bda; | ||||
|     memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); | ||||
|     if (has_simple_value_) { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_simple_); | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_.simple); | ||||
|     } else { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_value_template(std::function<bool(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   void set_value_template(bool (*func)(Ts...)) { | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const bool &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   bool value_simple_{false}; | ||||
|   std::function<bool(Ts...)> value_template_{}; | ||||
|   union { | ||||
|     bool simple; | ||||
|     bool (*template_func)(Ts...); | ||||
|   } value_{.simple = false}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { | ||||
|   | ||||
| @@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga | ||||
| } | ||||
|  | ||||
| float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { | ||||
|   if (this->data_to_value_func_.has_value()) { | ||||
|   if (this->has_data_to_value_) { | ||||
|     std::vector<uint8_t> data(value, value + value_len); | ||||
|     return (*this->data_to_value_func_)(data); | ||||
|     return this->data_to_value_func_(data); | ||||
|   } else { | ||||
|     return value[0]; | ||||
|   } | ||||
|   | ||||
| @@ -15,8 +15,6 @@ namespace ble_client { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| using data_to_value_t = std::function<float(std::vector<uint8_t>)>; | ||||
|  | ||||
| class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { | ||||
|  public: | ||||
|   void loop() override; | ||||
| @@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie | ||||
|   void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } | ||||
|   void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|   void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } | ||||
|   void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) { | ||||
|     this->data_to_value_func_ = lambda; | ||||
|     this->has_data_to_value_ = true; | ||||
|   } | ||||
|   void set_enable_notify(bool notify) { this->notify_ = notify; } | ||||
|   uint16_t handle; | ||||
|  | ||||
|  protected: | ||||
|   float parse_data_(uint8_t *value, uint16_t value_len); | ||||
|   optional<data_to_value_t> data_to_value_func_{}; | ||||
|   bool has_data_to_value_{false}; | ||||
|   float (*data_to_value_func_)(const std::vector<uint8_t> &){}; | ||||
|   bool notify_; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   | ||||
							
								
								
									
										29
									
								
								esphome/components/ble_nus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/ble_nus/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.zephyr import zephyr_add_prj_conf | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE | ||||
|  | ||||
| AUTO_LOAD = ["zephyr_ble_server"] | ||||
| CODEOWNERS = ["@tomaszduda23"] | ||||
|  | ||||
| ble_nus_ns = cg.esphome_ns.namespace("ble_nus") | ||||
| BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BLENUS), | ||||
|             cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( | ||||
|                 *[CONF_LOGS], lower=True | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.only_with_framework("zephyr"), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     zephyr_add_prj_conf("BT_NUS", True) | ||||
|     cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS)) | ||||
|     await cg.register_component(var, config) | ||||
							
								
								
									
										157
									
								
								esphome/components/ble_nus/ble_nus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/ble_nus/ble_nus.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| #ifdef USE_ZEPHYR | ||||
| #include "ble_nus.h" | ||||
| #include <zephyr/kernel.h> | ||||
| #include <bluetooth/services/nus.h> | ||||
| #include "esphome/core/log.h" | ||||
| #ifdef USE_LOGGER | ||||
| #include "esphome/components/logger/logger.h" | ||||
| #include "esphome/core/application.h" | ||||
| #endif | ||||
| #include <zephyr/sys/ring_buffer.h> | ||||
|  | ||||
| namespace esphome::ble_nus { | ||||
|  | ||||
| constexpr size_t BLE_TX_BUF_SIZE = 2048; | ||||
|  | ||||
| // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| BLENUS *global_ble_nus; | ||||
| RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); | ||||
| // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| static const char *const TAG = "ble_nus"; | ||||
|  | ||||
| size_t BLENUS::write_array(const uint8_t *data, size_t len) { | ||||
|   if (atomic_get(&this->tx_status_) == TX_DISABLED) { | ||||
|     return 0; | ||||
|   } | ||||
|   return ring_buf_put(&global_ble_tx_ring_buf, data, len); | ||||
| } | ||||
|  | ||||
| void BLENUS::connected(bt_conn *conn, uint8_t err) { | ||||
|   if (err == 0) { | ||||
|     global_ble_nus->conn_.store(bt_conn_ref(conn)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { | ||||
|   if (global_ble_nus->conn_) { | ||||
|     bt_conn_unref(global_ble_nus->conn_.load()); | ||||
|     // Connection array is global static. | ||||
|     // Reference can be kept even if disconnected. | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLENUS::tx_callback(bt_conn *conn) { | ||||
|   atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED); | ||||
|   ESP_LOGVV(TAG, "Sent operation completed"); | ||||
| } | ||||
|  | ||||
| void BLENUS::send_enabled_callback(bt_nus_send_status status) { | ||||
|   switch (status) { | ||||
|     case BT_NUS_SEND_STATUS_ENABLED: | ||||
|       atomic_set(&global_ble_nus->tx_status_, TX_ENABLED); | ||||
| #ifdef USE_LOGGER | ||||
|       if (global_ble_nus->expose_log_) { | ||||
|         App.schedule_dump_config(); | ||||
|       } | ||||
| #endif | ||||
|       ESP_LOGD(TAG, "NUS notification has been enabled"); | ||||
|       break; | ||||
|     case BT_NUS_SEND_STATUS_DISABLED: | ||||
|       atomic_set(&global_ble_nus->tx_status_, TX_DISABLED); | ||||
|       ESP_LOGD(TAG, "NUS notification has been disabled"); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { | ||||
|   ESP_LOGD(TAG, "Received %d bytes.", len); | ||||
| } | ||||
|  | ||||
| void BLENUS::setup() { | ||||
|   bt_nus_cb callbacks = { | ||||
|       .received = rx_callback, | ||||
|       .sent = tx_callback, | ||||
|       .send_enabled = send_enabled_callback, | ||||
|   }; | ||||
|  | ||||
|   bt_nus_init(&callbacks); | ||||
|  | ||||
|   static bt_conn_cb conn_callbacks = { | ||||
|       .connected = BLENUS::connected, | ||||
|       .disconnected = BLENUS::disconnected, | ||||
|   }; | ||||
|  | ||||
|   bt_conn_cb_register(&conn_callbacks); | ||||
|  | ||||
|   global_ble_nus = this; | ||||
| #ifdef USE_LOGGER | ||||
|   if (logger::global_logger != nullptr && this->expose_log_) { | ||||
|     logger::global_logger->add_on_log_callback( | ||||
|         [this](int level, const char *tag, const char *message, size_t message_len) { | ||||
|           this->write_array(reinterpret_cast<const uint8_t *>(message), message_len); | ||||
|           const char c = '\n'; | ||||
|           this->write_array(reinterpret_cast<const uint8_t *>(&c), 1); | ||||
|         }); | ||||
|   } | ||||
|  | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void BLENUS::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ble nus:"); | ||||
|   ESP_LOGCONFIG(TAG, "  log: %s", YESNO(this->expose_log_)); | ||||
|   uint32_t mtu = 0; | ||||
|   bt_conn *conn = this->conn_.load(); | ||||
|   if (conn) { | ||||
|     mtu = bt_nus_get_mtu(conn); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  MTU: %u", mtu); | ||||
| } | ||||
|  | ||||
| void BLENUS::loop() { | ||||
|   if (ring_buf_is_empty(&global_ble_tx_ring_buf)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) { | ||||
|     if (atomic_get(&this->tx_status_) == TX_DISABLED) { | ||||
|       ring_buf_reset(&global_ble_tx_ring_buf); | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   bt_conn *conn = this->conn_.load(); | ||||
|   if (conn) { | ||||
|     conn = bt_conn_ref(conn); | ||||
|   } | ||||
|  | ||||
|   if (nullptr == conn) { | ||||
|     atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint32_t req_len = bt_nus_get_mtu(conn); | ||||
|  | ||||
|   uint8_t *buf; | ||||
|   uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len); | ||||
|  | ||||
|   int err, err2; | ||||
|  | ||||
|   err = bt_nus_send(conn, buf, size); | ||||
|   err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size); | ||||
|   if (err2) { | ||||
|     // It should no happen. | ||||
|     ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2); | ||||
|   } | ||||
|   if (err == 0) { | ||||
|     ESP_LOGVV(TAG, "Sent %d bytes", size); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err); | ||||
|     atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED); | ||||
|   } | ||||
|   bt_conn_unref(conn); | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::ble_nus | ||||
| #endif | ||||
							
								
								
									
										37
									
								
								esphome/components/ble_nus/ble_nus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/ble_nus/ble_nus.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| #pragma once | ||||
| #ifdef USE_ZEPHYR | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include <shell/shell_bt_nus.h> | ||||
| #include <atomic> | ||||
|  | ||||
| namespace esphome::ble_nus { | ||||
|  | ||||
| class BLENUS : public Component { | ||||
|   enum TxStatus { | ||||
|     TX_DISABLED, | ||||
|     TX_ENABLED, | ||||
|     TX_BUSY, | ||||
|   }; | ||||
|  | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|   size_t write_array(const uint8_t *data, size_t len); | ||||
|   void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } | ||||
|  | ||||
|  protected: | ||||
|   static void send_enabled_callback(bt_nus_send_status status); | ||||
|   static void tx_callback(bt_conn *conn); | ||||
|   static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len); | ||||
|   static void connected(bt_conn *conn, uint8_t err); | ||||
|   static void disconnected(bt_conn *conn, uint8_t reason); | ||||
|  | ||||
|   std::atomic<bt_conn *> conn_ = nullptr; | ||||
|   bool expose_log_ = false; | ||||
|   atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::ble_nus | ||||
| #endif | ||||
| @@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() { | ||||
|     service_resp.handle = service_result.start_handle; | ||||
|  | ||||
|     if (total_char_count > 0) { | ||||
|       // Reserve space and process characteristics | ||||
|       service_resp.characteristics.reserve(total_char_count); | ||||
|       // Initialize FixedVector with exact count and process characteristics | ||||
|       service_resp.characteristics.init(total_char_count); | ||||
|       uint16_t char_offset = 0; | ||||
|       esp_gattc_char_elem_t char_result; | ||||
|       while (true) {  // characteristics | ||||
| @@ -253,9 +253,7 @@ void BluetoothConnection::send_service_for_discovery_() { | ||||
|  | ||||
|         service_resp.characteristics.emplace_back(); | ||||
|         auto &characteristic_resp = service_resp.characteristics.back(); | ||||
|  | ||||
|         fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); | ||||
|  | ||||
|         characteristic_resp.handle = char_result.char_handle; | ||||
|         characteristic_resp.properties = char_result.properties; | ||||
|         char_offset++; | ||||
| @@ -271,12 +269,11 @@ void BluetoothConnection::send_service_for_discovery_() { | ||||
|           return; | ||||
|         } | ||||
|         if (total_desc_count == 0) { | ||||
|           // No descriptors, continue to next characteristic | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         // Reserve space and process descriptors | ||||
|         characteristic_resp.descriptors.reserve(total_desc_count); | ||||
|         // Initialize FixedVector with exact count and process descriptors | ||||
|         characteristic_resp.descriptors.init(total_desc_count); | ||||
|         uint16_t desc_offset = 0; | ||||
|         esp_gattc_descr_elem_t desc_result; | ||||
|         while (true) {  // descriptors | ||||
| @@ -297,9 +294,7 @@ void BluetoothConnection::send_service_for_discovery_() { | ||||
|  | ||||
|           characteristic_resp.descriptors.emplace_back(); | ||||
|           auto &descriptor_resp = characteristic_resp.descriptors.back(); | ||||
|  | ||||
|           fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids); | ||||
|  | ||||
|           descriptor_resp.handle = desc_result.handle; | ||||
|           desc_offset++; | ||||
|         } | ||||
|   | ||||
| @@ -155,16 +155,12 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par | ||||
| BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { | ||||
|   for (uint8_t i = 0; i < this->connection_count_; i++) { | ||||
|     auto *connection = this->connections_[i]; | ||||
|     if (connection->get_address() == address) | ||||
|     uint64_t conn_addr = connection->get_address(); | ||||
|  | ||||
|     if (conn_addr == address) | ||||
|       return connection; | ||||
|   } | ||||
|  | ||||
|   if (!reserve) | ||||
|     return nullptr; | ||||
|  | ||||
|   for (uint8_t i = 0; i < this->connection_count_; i++) { | ||||
|     auto *connection = this->connections_[i]; | ||||
|     if (connection->get_address() == 0) { | ||||
|     if (reserve && conn_addr == 0) { | ||||
|       connection->send_service_ = INIT_SENDING_SERVICES; | ||||
|       connection->set_address(address); | ||||
|       // All connections must start at INIT | ||||
| @@ -175,7 +171,6 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese | ||||
|       return connection; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return nullptr; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,9 @@ | ||||
|  | ||||
| #include "bluetooth_connection.h" | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #include <esp_bt.h> | ||||
| #endif | ||||
| #include <esp_bt_device.h> | ||||
|  | ||||
| namespace esphome::bluetooth_proxy { | ||||
|   | ||||
| @@ -84,11 +84,6 @@ def button_schema( | ||||
|     return _BUTTON_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| BUTTON_SCHEMA = button_schema(Button) | ||||
| BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) | ||||
|  | ||||
|  | ||||
| async def setup_button_core_(var, config): | ||||
|     await setup_entity(var, config, "button") | ||||
|  | ||||
|   | ||||
| @@ -8,17 +8,30 @@ namespace cap1188 { | ||||
| static const char *const TAG = "cap1188"; | ||||
|  | ||||
| void CAP1188Component::setup() { | ||||
|   // Reset device using the reset pin | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(100);  // NOLINT | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     delay(100);  // NOLINT | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(100);  // NOLINT | ||||
|   this->disable_loop(); | ||||
|  | ||||
|   // no reset pin | ||||
|   if (this->reset_pin_ == nullptr) { | ||||
|     this->finish_setup_(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // reset pin configured so reset before finishing setup | ||||
|   this->reset_pin_->setup(); | ||||
|   this->reset_pin_->digital_write(false); | ||||
|   // delay after reset pin write | ||||
|   this->set_timeout(100, [this]() { | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     // delay after reset pin write | ||||
|     this->set_timeout(100, [this]() { | ||||
|       this->reset_pin_->digital_write(false); | ||||
|       // delay after reset pin write | ||||
|       this->set_timeout(100, [this]() { this->finish_setup_(); }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void CAP1188Component::finish_setup_() { | ||||
|   // Check if CAP1188 is actually connected | ||||
|   this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_); | ||||
|   this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_); | ||||
| @@ -44,6 +57,9 @@ void CAP1188Component::setup() { | ||||
|  | ||||
|   // Speed up a bit | ||||
|   this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30); | ||||
|  | ||||
|   // Setup successful, so enable loop | ||||
|   this->enable_loop(); | ||||
| } | ||||
|  | ||||
| void CAP1188Component::dump_config() { | ||||
|   | ||||
| @@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice { | ||||
|   void loop() override; | ||||
|  | ||||
|  protected: | ||||
|   void finish_setup_(); | ||||
|  | ||||
|   std::vector<CAP1188Channel *> channels_{}; | ||||
|   uint8_t touch_threshold_{0x20}; | ||||
|   uint8_t allow_multiple_touches_{0x80}; | ||||
|   | ||||
| @@ -270,11 +270,6 @@ def climate_schema( | ||||
|     return _CLIMATE_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| CLIMATE_SCHEMA = climate_schema(Climate) | ||||
| CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) | ||||
|  | ||||
|  | ||||
| async def setup_climate_core_(var, config): | ||||
|     await setup_entity(var, config, "climate") | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,42 @@ namespace climate { | ||||
|  | ||||
| static const char *const TAG = "climate"; | ||||
|  | ||||
| // Memory-efficient lookup tables | ||||
| struct StringToUint8 { | ||||
|   const char *str; | ||||
|   const uint8_t value; | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = { | ||||
|     {"OFF", CLIMATE_MODE_OFF}, | ||||
|     {"AUTO", CLIMATE_MODE_AUTO}, | ||||
|     {"COOL", CLIMATE_MODE_COOL}, | ||||
|     {"HEAT", CLIMATE_MODE_HEAT}, | ||||
|     {"FAN_ONLY", CLIMATE_MODE_FAN_ONLY}, | ||||
|     {"DRY", CLIMATE_MODE_DRY}, | ||||
|     {"HEAT_COOL", CLIMATE_MODE_HEAT_COOL}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = { | ||||
|     {"ON", CLIMATE_FAN_ON},         {"OFF", CLIMATE_FAN_OFF},       {"AUTO", CLIMATE_FAN_AUTO}, | ||||
|     {"LOW", CLIMATE_FAN_LOW},       {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH}, | ||||
|     {"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS},   {"DIFFUSE", CLIMATE_FAN_DIFFUSE}, | ||||
|     {"QUIET", CLIMATE_FAN_QUIET}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = { | ||||
|     {"ECO", CLIMATE_PRESET_ECO},           {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST}, | ||||
|     {"COMFORT", CLIMATE_PRESET_COMFORT},   {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP}, | ||||
|     {"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE}, | ||||
| }; | ||||
|  | ||||
| constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { | ||||
|     {"OFF", CLIMATE_SWING_OFF}, | ||||
|     {"BOTH", CLIMATE_SWING_BOTH}, | ||||
|     {"VERTICAL", CLIMATE_SWING_VERTICAL}, | ||||
|     {"HORIZONTAL", CLIMATE_SWING_HORIZONTAL}, | ||||
| }; | ||||
|  | ||||
| void ClimateCall::perform() { | ||||
|   this->parent_->control_callback_.call(*this); | ||||
|   ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); | ||||
| @@ -50,206 +86,175 @@ void ClimateCall::perform() { | ||||
|   } | ||||
|   this->parent_->control(*this); | ||||
| } | ||||
|  | ||||
| void ClimateCall::validate_() { | ||||
|   auto traits = this->parent_->get_traits(); | ||||
|   if (this->mode_.has_value()) { | ||||
|     auto mode = *this->mode_; | ||||
|     if (!traits.supports_mode(mode)) { | ||||
|       ESP_LOGW(TAG, "  Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||
|       ESP_LOGW(TAG, "  Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||
|       this->mode_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->custom_fan_mode_.has_value()) { | ||||
|     auto custom_fan_mode = *this->custom_fan_mode_; | ||||
|     if (!traits.supports_custom_fan_mode(custom_fan_mode)) { | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", custom_fan_mode.c_str()); | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s not supported", custom_fan_mode.c_str()); | ||||
|       this->custom_fan_mode_.reset(); | ||||
|     } | ||||
|   } else if (this->fan_mode_.has_value()) { | ||||
|     auto fan_mode = *this->fan_mode_; | ||||
|     if (!traits.supports_fan_mode(fan_mode)) { | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", | ||||
|                LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||
|       this->fan_mode_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->custom_preset_.has_value()) { | ||||
|     auto custom_preset = *this->custom_preset_; | ||||
|     if (!traits.supports_custom_preset(custom_preset)) { | ||||
|       ESP_LOGW(TAG, "  Preset %s is not supported by this device!", custom_preset.c_str()); | ||||
|       ESP_LOGW(TAG, "  Preset %s not supported", custom_preset.c_str()); | ||||
|       this->custom_preset_.reset(); | ||||
|     } | ||||
|   } else if (this->preset_.has_value()) { | ||||
|     auto preset = *this->preset_; | ||||
|     if (!traits.supports_preset(preset)) { | ||||
|       ESP_LOGW(TAG, "  Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|       ESP_LOGW(TAG, "  Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|       this->preset_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->swing_mode_.has_value()) { | ||||
|     auto swing_mode = *this->swing_mode_; | ||||
|     if (!traits.supports_swing_mode(swing_mode)) { | ||||
|       ESP_LOGW(TAG, "  Swing Mode %s is not supported by this device!", | ||||
|                LOG_STR_ARG(climate_swing_mode_to_string(swing_mode))); | ||||
|       ESP_LOGW(TAG, "  Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode))); | ||||
|       this->swing_mode_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->target_temperature_.has_value()) { | ||||
|     auto target = *this->target_temperature_; | ||||
|     if (traits.get_supports_two_point_target_temperature()) { | ||||
|     if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                  CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|       ESP_LOGW(TAG, "  Cannot set target temperature for climate device " | ||||
|                     "with two-point target temperature!"); | ||||
|                     "with two-point target temperature"); | ||||
|       this->target_temperature_.reset(); | ||||
|     } else if (std::isnan(target)) { | ||||
|       ESP_LOGW(TAG, "  Target temperature must not be NAN!"); | ||||
|       ESP_LOGW(TAG, "  Target temperature must not be NAN"); | ||||
|       this->target_temperature_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) { | ||||
|     if (!traits.get_supports_two_point_target_temperature()) { | ||||
|       ESP_LOGW(TAG, "  Cannot set low/high target temperature for this device!"); | ||||
|     if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                   CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|       ESP_LOGW(TAG, "  Cannot set low/high target temperature"); | ||||
|       this->target_temperature_low_.reset(); | ||||
|       this->target_temperature_high_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) { | ||||
|     ESP_LOGW(TAG, "  Target temperature low must not be NAN!"); | ||||
|     ESP_LOGW(TAG, "  Target temperature low must not be NAN"); | ||||
|     this->target_temperature_low_.reset(); | ||||
|   } | ||||
|   if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) { | ||||
|     ESP_LOGW(TAG, "  Target temperature low must not be NAN!"); | ||||
|     ESP_LOGW(TAG, "  Target temperature high must not be NAN"); | ||||
|     this->target_temperature_high_.reset(); | ||||
|   } | ||||
|   if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) { | ||||
|     float low = *this->target_temperature_low_; | ||||
|     float high = *this->target_temperature_high_; | ||||
|     if (low > high) { | ||||
|       ESP_LOGW(TAG, "  Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high); | ||||
|       ESP_LOGW(TAG, "  Target temperature low %.2f must be less than target temperature high %.2f", low, high); | ||||
|       this->target_temperature_low_.reset(); | ||||
|       this->target_temperature_high_.reset(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_mode(ClimateMode mode) { | ||||
|   this->mode_ = mode; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_mode(const std::string &mode) { | ||||
|   if (str_equals_case_insensitive(mode, "OFF")) { | ||||
|     this->set_mode(CLIMATE_MODE_OFF); | ||||
|   } else if (str_equals_case_insensitive(mode, "AUTO")) { | ||||
|     this->set_mode(CLIMATE_MODE_AUTO); | ||||
|   } else if (str_equals_case_insensitive(mode, "COOL")) { | ||||
|     this->set_mode(CLIMATE_MODE_COOL); | ||||
|   } else if (str_equals_case_insensitive(mode, "HEAT")) { | ||||
|     this->set_mode(CLIMATE_MODE_HEAT); | ||||
|   } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) { | ||||
|     this->set_mode(CLIMATE_MODE_FAN_ONLY); | ||||
|   } else if (str_equals_case_insensitive(mode, "DRY")) { | ||||
|     this->set_mode(CLIMATE_MODE_DRY); | ||||
|   } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) { | ||||
|     this->set_mode(CLIMATE_MODE_HEAT_COOL); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); | ||||
|   for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { | ||||
|     if (str_equals_case_insensitive(mode, mode_entry.str)) { | ||||
|       this->set_mode(static_cast<ClimateMode>(mode_entry.value)); | ||||
|       return *this; | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { | ||||
|   this->fan_mode_ = fan_mode; | ||||
|   this->custom_fan_mode_.reset(); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { | ||||
|   if (str_equals_case_insensitive(fan_mode, "ON")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_ON); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "OFF")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_OFF); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_AUTO); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "LOW")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_LOW); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_MEDIUM); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "HIGH")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_HIGH); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_MIDDLE); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_FOCUS); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_DIFFUSE); | ||||
|   } else if (str_equals_case_insensitive(fan_mode, "QUIET")) { | ||||
|     this->set_fan_mode(CLIMATE_FAN_QUIET); | ||||
|   } else { | ||||
|     if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { | ||||
|       this->custom_fan_mode_ = fan_mode; | ||||
|       this->fan_mode_.reset(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); | ||||
|   for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { | ||||
|     if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { | ||||
|       this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); | ||||
|       return *this; | ||||
|     } | ||||
|   } | ||||
|   if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { | ||||
|     this->custom_fan_mode_ = fan_mode; | ||||
|     this->fan_mode_.reset(); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { | ||||
|   if (fan_mode.has_value()) { | ||||
|     this->set_fan_mode(fan_mode.value()); | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { | ||||
|   this->preset_ = preset; | ||||
|   this->custom_preset_.reset(); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(const std::string &preset) { | ||||
|   if (str_equals_case_insensitive(preset, "ECO")) { | ||||
|     this->set_preset(CLIMATE_PRESET_ECO); | ||||
|   } else if (str_equals_case_insensitive(preset, "AWAY")) { | ||||
|     this->set_preset(CLIMATE_PRESET_AWAY); | ||||
|   } else if (str_equals_case_insensitive(preset, "BOOST")) { | ||||
|     this->set_preset(CLIMATE_PRESET_BOOST); | ||||
|   } else if (str_equals_case_insensitive(preset, "COMFORT")) { | ||||
|     this->set_preset(CLIMATE_PRESET_COMFORT); | ||||
|   } else if (str_equals_case_insensitive(preset, "HOME")) { | ||||
|     this->set_preset(CLIMATE_PRESET_HOME); | ||||
|   } else if (str_equals_case_insensitive(preset, "SLEEP")) { | ||||
|     this->set_preset(CLIMATE_PRESET_SLEEP); | ||||
|   } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { | ||||
|     this->set_preset(CLIMATE_PRESET_ACTIVITY); | ||||
|   } else if (str_equals_case_insensitive(preset, "NONE")) { | ||||
|     this->set_preset(CLIMATE_PRESET_NONE); | ||||
|   } else { | ||||
|     if (this->parent_->get_traits().supports_custom_preset(preset)) { | ||||
|       this->custom_preset_ = preset; | ||||
|       this->preset_.reset(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); | ||||
|   for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { | ||||
|     if (str_equals_case_insensitive(preset, preset_entry.str)) { | ||||
|       this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); | ||||
|       return *this; | ||||
|     } | ||||
|   } | ||||
|   if (this->parent_->get_traits().supports_custom_preset(preset)) { | ||||
|     this->custom_preset_ = preset; | ||||
|     this->preset_.reset(); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { | ||||
|   if (preset.has_value()) { | ||||
|     this->set_preset(preset.value()); | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { | ||||
|   this->swing_mode_ = swing_mode; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { | ||||
|   if (str_equals_case_insensitive(swing_mode, "OFF")) { | ||||
|     this->set_swing_mode(CLIMATE_SWING_OFF); | ||||
|   } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { | ||||
|     this->set_swing_mode(CLIMATE_SWING_BOTH); | ||||
|   } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) { | ||||
|     this->set_swing_mode(CLIMATE_SWING_VERTICAL); | ||||
|   } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) { | ||||
|     this->set_swing_mode(CLIMATE_SWING_HORIZONTAL); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); | ||||
|   for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) { | ||||
|     if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { | ||||
|       this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value)); | ||||
|       return *this; | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| @@ -257,59 +262,71 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { | ||||
|   this->target_temperature_ = target_temperature; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) { | ||||
|   this->target_temperature_low_ = target_temperature_low; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) { | ||||
|   this->target_temperature_high_ = target_temperature_high; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { | ||||
|   this->target_humidity_ = target_humidity; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } | ||||
| const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } | ||||
| const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } | ||||
| const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } | ||||
| const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; } | ||||
|  | ||||
| const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } | ||||
| const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||
| const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; } | ||||
| const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } | ||||
| const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { | ||||
|   this->target_temperature_high_ = target_temperature_high; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) { | ||||
|   this->target_temperature_low_ = target_temperature_low; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) { | ||||
|   this->target_temperature_ = target_temperature; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) { | ||||
|   this->target_humidity_ = target_humidity; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ||||
|   this->mode_ = mode; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { | ||||
|   this->fan_mode_ = fan_mode; | ||||
|   this->custom_fan_mode_.reset(); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { | ||||
|   this->preset_ = preset; | ||||
|   this->custom_preset_.reset(); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) { | ||||
|   this->swing_mode_ = swing_mode; | ||||
|   return *this; | ||||
| @@ -334,6 +351,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() { | ||||
|     return {}; | ||||
|   return recovered; | ||||
| } | ||||
|  | ||||
| void Climate::save_state_() { | ||||
| #if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ | ||||
|     !defined(CLANG_TIDY) | ||||
| @@ -350,13 +368,14 @@ void Climate::save_state_() { | ||||
|  | ||||
|   state.mode = this->mode; | ||||
|   auto traits = this->get_traits(); | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     state.target_temperature_low = this->target_temperature_low; | ||||
|     state.target_temperature_high = this->target_temperature_high; | ||||
|   } else { | ||||
|     state.target_temperature = this->target_temperature; | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     state.target_humidity = this->target_humidity; | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() && fan_mode.has_value()) { | ||||
| @@ -366,12 +385,14 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { | ||||
|     state.uses_custom_fan_mode = true; | ||||
|     const auto &supported = traits.get_supported_custom_fan_modes(); | ||||
|     std::vector<std::string> vec{supported.begin(), supported.end()}; | ||||
|     for (size_t i = 0; i < vec.size(); i++) { | ||||
|       if (vec[i] == custom_fan_mode) { | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : supported) { | ||||
|       if (mode == custom_fan_mode) { | ||||
|         state.custom_fan_mode = i; | ||||
|         break; | ||||
|       } | ||||
|       i++; | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_presets() && preset.has_value()) { | ||||
| @@ -381,12 +402,14 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { | ||||
|     state.uses_custom_preset = true; | ||||
|     const auto &supported = traits.get_supported_custom_presets(); | ||||
|     std::vector<std::string> vec{supported.begin(), supported.end()}; | ||||
|     for (size_t i = 0; i < vec.size(); i++) { | ||||
|       if (vec[i] == custom_preset) { | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &preset : supported) { | ||||
|       if (preset == custom_preset) { | ||||
|         state.custom_preset = i; | ||||
|         break; | ||||
|       } | ||||
|       i++; | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
| @@ -395,12 +418,13 @@ void Climate::save_state_() { | ||||
|  | ||||
|   this->rtc_.save(&state); | ||||
| } | ||||
|  | ||||
| void Climate::publish_state() { | ||||
|   ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   ESP_LOGD(TAG, "  Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); | ||||
|   if (traits.get_supports_action()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { | ||||
|     ESP_LOGD(TAG, "  Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { | ||||
| @@ -418,19 +442,20 @@ void Climate::publish_state() { | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     ESP_LOGD(TAG, "  Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); | ||||
|   } | ||||
|   if (traits.get_supports_current_temperature()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { | ||||
|     ESP_LOGD(TAG, "  Current Temperature: %.2f°C", this->current_temperature); | ||||
|   } | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     ESP_LOGD(TAG, "  Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, | ||||
|              this->target_temperature_high); | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "  Target Temperature: %.2f°C", this->target_temperature); | ||||
|   } | ||||
|   if (traits.get_supports_current_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { | ||||
|     ESP_LOGD(TAG, "  Current Humidity: %.0f%%", this->current_humidity); | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     ESP_LOGD(TAG, "  Target Humidity: %.0f%%", this->target_humidity); | ||||
|   } | ||||
|  | ||||
| @@ -465,16 +490,20 @@ ClimateTraits Climate::get_traits() { | ||||
| void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { | ||||
|   this->visual_min_temperature_override_ = visual_min_temperature_override; | ||||
| } | ||||
|  | ||||
| void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { | ||||
|   this->visual_max_temperature_override_ = visual_max_temperature_override; | ||||
| } | ||||
|  | ||||
| void Climate::set_visual_temperature_step_override(float target, float current) { | ||||
|   this->visual_target_temperature_step_override_ = target; | ||||
|   this->visual_current_temperature_step_override_ = current; | ||||
| } | ||||
|  | ||||
| void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { | ||||
|   this->visual_min_humidity_override_ = visual_min_humidity_override; | ||||
| } | ||||
|  | ||||
| void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { | ||||
|   this->visual_max_humidity_override_ = visual_max_humidity_override; | ||||
| } | ||||
| @@ -485,61 +514,70 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   auto call = climate->make_call(); | ||||
|   auto traits = climate->get_traits(); | ||||
|   call.set_mode(this->mode); | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     call.set_target_temperature_low(this->target_temperature_low); | ||||
|     call.set_target_temperature_high(this->target_temperature_high); | ||||
|   } else { | ||||
|     call.set_target_temperature(this->target_temperature); | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     call.set_target_humidity(this->target_humidity); | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       call.fan_mode_.reset(); | ||||
|       call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|     call.set_fan_mode(this->fan_mode); | ||||
|   } | ||||
|   if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       call.preset_.reset(); | ||||
|       call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|     call.set_preset(this->preset); | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|     call.set_swing_mode(this->swing_mode); | ||||
|   } | ||||
|   return call; | ||||
| } | ||||
|  | ||||
| void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   auto traits = climate->get_traits(); | ||||
|   climate->mode = this->mode; | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     climate->target_temperature_low = this->target_temperature_low; | ||||
|     climate->target_temperature_high = this->target_temperature_high; | ||||
|   } else { | ||||
|     climate->target_temperature = this->target_temperature; | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     climate->target_humidity = this->target_humidity; | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       climate->fan_mode.reset(); | ||||
|       climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|     climate->fan_mode = this->fan_mode; | ||||
|     climate->custom_fan_mode.reset(); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { | ||||
|     // std::set has consistent order (lexicographic for strings), so this is ok | ||||
|     const auto &modes = traits.get_supported_custom_fan_modes(); | ||||
|     std::vector<std::string> modes_vec{modes.begin(), modes.end()}; | ||||
|     if (custom_fan_mode < modes_vec.size()) { | ||||
|       climate->custom_fan_mode = modes_vec[this->custom_fan_mode]; | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       climate->preset.reset(); | ||||
|       climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_presets() && !this->uses_custom_preset) { | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|     climate->preset = this->preset; | ||||
|     climate->custom_preset.reset(); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { | ||||
|     // std::set has consistent order (lexicographic for strings), so this is ok | ||||
|     const auto &presets = traits.get_supported_custom_presets(); | ||||
|     std::vector<std::string> presets_vec{presets.begin(), presets.end()}; | ||||
|     if (custom_preset < presets_vec.size()) { | ||||
|       climate->custom_preset = presets_vec[this->custom_preset]; | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|     climate->swing_mode = this->swing_mode; | ||||
|   } | ||||
|   climate->publish_state(); | ||||
| @@ -573,66 +611,68 @@ void Climate::dump_traits_(const char *tag) { | ||||
|   auto traits = this->get_traits(); | ||||
|   ESP_LOGCONFIG(tag, "ClimateTraits:"); | ||||
|   ESP_LOGCONFIG(tag, | ||||
|                 "  [x] Visual settings:\n" | ||||
|                 "      - Min temperature: %.1f\n" | ||||
|                 "      - Max temperature: %.1f\n" | ||||
|                 "      - Temperature step:\n" | ||||
|                 "          Target: %.1f", | ||||
|                 "  Visual settings:\n" | ||||
|                 "  - Min temperature: %.1f\n" | ||||
|                 "  - Max temperature: %.1f\n" | ||||
|                 "  - Temperature step:\n" | ||||
|                 "      Target: %.1f", | ||||
|                 traits.get_visual_min_temperature(), traits.get_visual_max_temperature(), | ||||
|                 traits.get_visual_target_temperature_step()); | ||||
|   if (traits.get_supports_current_temperature()) { | ||||
|     ESP_LOGCONFIG(tag, "          Current: %.1f", traits.get_visual_current_temperature_step()); | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { | ||||
|     ESP_LOGCONFIG(tag, "      Current: %.1f", traits.get_visual_current_temperature_step()); | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY | | ||||
|                                climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { | ||||
|     ESP_LOGCONFIG(tag, | ||||
|                   "      - Min humidity: %.0f\n" | ||||
|                   "      - Max humidity: %.0f", | ||||
|                   "  - Min humidity: %.0f\n" | ||||
|                   "  - Max humidity: %.0f", | ||||
|                   traits.get_visual_min_humidity(), traits.get_visual_max_humidity()); | ||||
|   } | ||||
|   if (traits.get_supports_two_point_target_temperature()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature"); | ||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||
|                                CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||
|     ESP_LOGCONFIG(tag, "  Supports two-point target temperature"); | ||||
|   } | ||||
|   if (traits.get_supports_current_temperature()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supports current temperature"); | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { | ||||
|     ESP_LOGCONFIG(tag, "  Supports current temperature"); | ||||
|   } | ||||
|   if (traits.get_supports_target_humidity()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supports target humidity"); | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     ESP_LOGCONFIG(tag, "  Supports target humidity"); | ||||
|   } | ||||
|   if (traits.get_supports_current_humidity()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supports current humidity"); | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { | ||||
|     ESP_LOGCONFIG(tag, "  Supports current humidity"); | ||||
|   } | ||||
|   if (traits.get_supports_action()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supports action"); | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { | ||||
|     ESP_LOGCONFIG(tag, "  Supports action"); | ||||
|   } | ||||
|   if (!traits.get_supported_modes().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported modes:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported modes:"); | ||||
|     for (ClimateMode m : traits.get_supported_modes()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_mode_to_string(m))); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_mode_to_string(m))); | ||||
|   } | ||||
|   if (!traits.get_supported_fan_modes().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported fan modes:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported fan modes:"); | ||||
|     for (ClimateFanMode m : traits.get_supported_fan_modes()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_fan_modes().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported custom fan modes:"); | ||||
|     for (const std::string &s : traits.get_supported_custom_fan_modes()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", s.c_str()); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", s.c_str()); | ||||
|   } | ||||
|   if (!traits.get_supported_presets().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported presets:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported presets:"); | ||||
|     for (ClimatePreset p : traits.get_supported_presets()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_preset_to_string(p))); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_preset_to_string(p))); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported custom presets:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported custom presets:"); | ||||
|     for (const std::string &s : traits.get_supported_custom_presets()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", s.c_str()); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", s.c_str()); | ||||
|   } | ||||
|   if (!traits.get_supported_swing_modes().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  [x] Supported swing modes:"); | ||||
|     ESP_LOGCONFIG(tag, "  Supported swing modes:"); | ||||
|     for (ClimateSwingMode m : traits.get_supported_swing_modes()) | ||||
|       ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); | ||||
|       ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,7 @@ class Climate; | ||||
| class ClimateCall { | ||||
|  public: | ||||
|   explicit ClimateCall(Climate *parent) : parent_(parent) {} | ||||
|   friend struct ClimateDeviceRestoreState; | ||||
|  | ||||
|   /// Set the mode of the climate device. | ||||
|   ClimateCall &set_mode(ClimateMode mode); | ||||
| @@ -93,30 +94,31 @@ class ClimateCall { | ||||
|  | ||||
|   void perform(); | ||||
|  | ||||
|   const optional<ClimateMode> &get_mode() const; | ||||
|   const optional<float> &get_target_temperature() const; | ||||
|   const optional<float> &get_target_temperature_low() const; | ||||
|   const optional<float> &get_target_temperature_high() const; | ||||
|   const optional<float> &get_target_humidity() const; | ||||
|  | ||||
|   const optional<ClimateMode> &get_mode() const; | ||||
|   const optional<ClimateFanMode> &get_fan_mode() const; | ||||
|   const optional<ClimateSwingMode> &get_swing_mode() const; | ||||
|   const optional<std::string> &get_custom_fan_mode() const; | ||||
|   const optional<ClimatePreset> &get_preset() const; | ||||
|   const optional<std::string> &get_custom_fan_mode() const; | ||||
|   const optional<std::string> &get_custom_preset() const; | ||||
|  | ||||
|  protected: | ||||
|   void validate_(); | ||||
|  | ||||
|   Climate *const parent_; | ||||
|   optional<ClimateMode> mode_; | ||||
|   optional<float> target_temperature_; | ||||
|   optional<float> target_temperature_low_; | ||||
|   optional<float> target_temperature_high_; | ||||
|   optional<float> target_humidity_; | ||||
|   optional<ClimateMode> mode_; | ||||
|   optional<ClimateFanMode> fan_mode_; | ||||
|   optional<ClimateSwingMode> swing_mode_; | ||||
|   optional<std::string> custom_fan_mode_; | ||||
|   optional<ClimatePreset> preset_; | ||||
|   optional<std::string> custom_fan_mode_; | ||||
|   optional<std::string> custom_preset_; | ||||
| }; | ||||
|  | ||||
| @@ -169,47 +171,6 @@ class Climate : public EntityBase { | ||||
|  public: | ||||
|   Climate() {} | ||||
|  | ||||
|   /// The active mode of the climate device. | ||||
|   ClimateMode mode{CLIMATE_MODE_OFF}; | ||||
|  | ||||
|   /// The active state of the climate device. | ||||
|   ClimateAction action{CLIMATE_ACTION_OFF}; | ||||
|  | ||||
|   /// The current temperature of the climate device, as reported from the integration. | ||||
|   float current_temperature{NAN}; | ||||
|  | ||||
|   /// The current humidity of the climate device, as reported from the integration. | ||||
|   float current_humidity{NAN}; | ||||
|  | ||||
|   union { | ||||
|     /// The target temperature of the climate device. | ||||
|     float target_temperature; | ||||
|     struct { | ||||
|       /// The minimum target temperature of the climate device, for climate devices with split target temperature. | ||||
|       float target_temperature_low{NAN}; | ||||
|       /// The maximum target temperature of the climate device, for climate devices with split target temperature. | ||||
|       float target_temperature_high{NAN}; | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   /// The target humidity of the climate device. | ||||
|   float target_humidity; | ||||
|  | ||||
|   /// The active fan mode of the climate device. | ||||
|   optional<ClimateFanMode> fan_mode; | ||||
|  | ||||
|   /// The active swing mode of the climate device. | ||||
|   ClimateSwingMode swing_mode; | ||||
|  | ||||
|   /// The active custom fan mode of the climate device. | ||||
|   optional<std::string> custom_fan_mode; | ||||
|  | ||||
|   /// The active preset of the climate device. | ||||
|   optional<ClimatePreset> preset; | ||||
|  | ||||
|   /// The active custom preset mode of the climate device. | ||||
|   optional<std::string> custom_preset; | ||||
|  | ||||
|   /** Add a callback for the climate device state, each time the state of the climate device is updated | ||||
|    * (using publish_state), this callback will be called. | ||||
|    * | ||||
| @@ -251,6 +212,47 @@ class Climate : public EntityBase { | ||||
|   void set_visual_min_humidity_override(float visual_min_humidity_override); | ||||
|   void set_visual_max_humidity_override(float visual_max_humidity_override); | ||||
|  | ||||
|   /// The current temperature of the climate device, as reported from the integration. | ||||
|   float current_temperature{NAN}; | ||||
|  | ||||
|   /// The current humidity of the climate device, as reported from the integration. | ||||
|   float current_humidity{NAN}; | ||||
|  | ||||
|   union { | ||||
|     /// The target temperature of the climate device. | ||||
|     float target_temperature; | ||||
|     struct { | ||||
|       /// The minimum target temperature of the climate device, for climate devices with split target temperature. | ||||
|       float target_temperature_low{NAN}; | ||||
|       /// The maximum target temperature of the climate device, for climate devices with split target temperature. | ||||
|       float target_temperature_high{NAN}; | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   /// The target humidity of the climate device. | ||||
|   float target_humidity; | ||||
|  | ||||
|   /// The active fan mode of the climate device. | ||||
|   optional<ClimateFanMode> fan_mode; | ||||
|  | ||||
|   /// The active preset of the climate device. | ||||
|   optional<ClimatePreset> preset; | ||||
|  | ||||
|   /// The active custom fan mode of the climate device. | ||||
|   optional<std::string> custom_fan_mode; | ||||
|  | ||||
|   /// The active custom preset mode of the climate device. | ||||
|   optional<std::string> custom_preset; | ||||
|  | ||||
|   /// The active mode of the climate device. | ||||
|   ClimateMode mode{CLIMATE_MODE_OFF}; | ||||
|  | ||||
|   /// The active state of the climate device. | ||||
|   ClimateAction action{CLIMATE_ACTION_OFF}; | ||||
|  | ||||
|   /// The active swing mode of the climate device. | ||||
|   ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; | ||||
|  | ||||
|  protected: | ||||
|   friend ClimateCall; | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ namespace esphome { | ||||
| namespace climate { | ||||
|  | ||||
| /// Enum for all modes a climate device can be in. | ||||
| /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateMode : uint8_t { | ||||
|   /// The climate device is off | ||||
|   CLIMATE_MODE_OFF = 0, | ||||
| @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { | ||||
|    * For example, the target temperature can be adjusted based on a schedule, or learned behavior. | ||||
|    * The target temperature can't be adjusted when in this mode. | ||||
|    */ | ||||
|   CLIMATE_MODE_AUTO = 6 | ||||
|   CLIMATE_MODE_AUTO = 6  // Update ClimateModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for the current action of the climate device. Values match those of ClimateMode. | ||||
| @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { | ||||
|   CLIMATE_ACTION_FAN = 6, | ||||
| }; | ||||
|  | ||||
| /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to On | ||||
|   CLIMATE_FAN_ON = 0, | ||||
| @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to Diffuse | ||||
|   CLIMATE_FAN_DIFFUSE = 8, | ||||
|   /// The fan mode is set to Quiet | ||||
|   CLIMATE_FAN_QUIET = 9, | ||||
|   CLIMATE_FAN_QUIET = 9,  // Update ClimateFanModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for all modes a climate swing can be in | ||||
| /// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateSwingMode : uint8_t { | ||||
|   /// The swing mode is set to Off | ||||
|   CLIMATE_SWING_OFF = 0, | ||||
| @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { | ||||
|   /// The fan mode is set to Vertical | ||||
|   CLIMATE_SWING_VERTICAL = 2, | ||||
|   /// The fan mode is set to Horizontal | ||||
|   CLIMATE_SWING_HORIZONTAL = 3, | ||||
|   CLIMATE_SWING_HORIZONTAL = 3,  // Update ClimateSwingModeMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| /// Enum for all preset modes | ||||
| /// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value | ||||
| enum ClimatePreset : uint8_t { | ||||
|   /// No preset is active | ||||
|   CLIMATE_PRESET_NONE = 0, | ||||
| @@ -95,7 +99,22 @@ enum ClimatePreset : uint8_t { | ||||
|   /// Device is prepared for sleep | ||||
|   CLIMATE_PRESET_SLEEP = 6, | ||||
|   /// Device is reacting to activity (e.g., movement sensors) | ||||
|   CLIMATE_PRESET_ACTIVITY = 7, | ||||
|   CLIMATE_PRESET_ACTIVITY = 7,  // Update ClimatePresetMask in climate_traits.h if adding values after this | ||||
| }; | ||||
|  | ||||
| enum ClimateFeature : uint32_t { | ||||
|   // Reporting current temperature is supported | ||||
|   CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0, | ||||
|   // Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL) | ||||
|   CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1, | ||||
|   // Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported) | ||||
|   CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2, | ||||
|   // Reporting current humidity is supported | ||||
|   CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3, | ||||
|   // Setting a target humidity is supported | ||||
|   CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4, | ||||
|   // Reporting current climate action is supported | ||||
|   CLIMATE_SUPPORTS_ACTION = 1 << 5, | ||||
| }; | ||||
|  | ||||
| /// Convert the given ClimateMode to a human-readable string. | ||||
|   | ||||
| @@ -1,19 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/helpers.h" | ||||
| #include <vector> | ||||
| #include "climate_mode.h" | ||||
| #include <set> | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace api { | ||||
| class APIConnection; | ||||
| }  // namespace api | ||||
| #endif | ||||
|  | ||||
| namespace climate { | ||||
|  | ||||
| // Type aliases for climate enum bitmasks | ||||
| // These replace std::set<EnumType> to eliminate red-black tree overhead | ||||
| // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) | ||||
| // Bitmask size is automatically calculated from the last enum value | ||||
| using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>; | ||||
| using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>; | ||||
| using ClimateSwingModeMask = | ||||
|     FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>; | ||||
| using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>; | ||||
|  | ||||
| // Lightweight linear search for small vectors (1-20 items) | ||||
| // Avoids std::find template overhead | ||||
| template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) { | ||||
|   for (const auto &item : vec) { | ||||
|     if (item == value) | ||||
|       return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| /** This class contains all static data for climate devices. | ||||
|  * | ||||
|  * All climate devices must support these features: | ||||
| @@ -21,135 +35,146 @@ namespace climate { | ||||
|  *  - Target Temperature | ||||
|  * | ||||
|  * All other properties and modes are optional and the integration must mark | ||||
|  * each of them as supported by setting the appropriate flag here. | ||||
|  * each of them as supported by setting the appropriate flag(s) here. | ||||
|  * | ||||
|  *  - supports current temperature - if the climate device supports reporting a current temperature | ||||
|  *  - supports two point target temperature - if the climate device's target temperature should be | ||||
|  *     split in target_temperature_low and target_temperature_high instead of just the single target_temperature | ||||
|  *  - feature flags: see ClimateFeatures enum in climate_mode.h | ||||
|  *  - supports modes: | ||||
|  *    - auto mode (automatic control) | ||||
|  *    - cool mode (lowers current temperature) | ||||
|  *    - heat mode (increases current temperature) | ||||
|  *    - dry mode (removes humidity from air) | ||||
|  *    - fan mode (only turns on fan) | ||||
|  *  - supports action - if the climate device supports reporting the active | ||||
|  *    current action of the device with the action property. | ||||
|  *  - supports fan modes - optionally, if it has a fan which can be configured in different ways: | ||||
|  *    - on, off, auto, high, medium, low, middle, focus, diffuse, quiet | ||||
|  *  - supports swing modes - optionally, if it has a swing which can be configured in different ways: | ||||
|  *    - off, both, vertical, horizontal | ||||
|  * | ||||
|  * This class also contains static data for the climate device display: | ||||
|  *  - visual min/max temperature - tells the frontend what range of temperatures the climate device | ||||
|  *     should display (gauge min/max values) | ||||
|  *  - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the | ||||
|  *     climate device should display (gauge min/max values) | ||||
|  *  - temperature step - the step with which to increase/decrease target temperature. | ||||
|  *     This also affects with how many decimal places the temperature is shown | ||||
|  */ | ||||
| class ClimateTraits { | ||||
|  public: | ||||
|   bool get_supports_current_temperature() const { return this->supports_current_temperature_; } | ||||
|   /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) | ||||
|   uint32_t get_feature_flags() const { return this->feature_flags_; } | ||||
|   void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } | ||||
|   void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } | ||||
|   bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; } | ||||
|   void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; } | ||||
|  | ||||
|   ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") | ||||
|   bool get_supports_current_temperature() const { | ||||
|     return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|   } | ||||
|   ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") | ||||
|   void set_supports_current_temperature(bool supports_current_temperature) { | ||||
|     this->supports_current_temperature_ = supports_current_temperature; | ||||
|     if (supports_current_temperature) { | ||||
|       this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|     } else { | ||||
|       this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|     } | ||||
|   } | ||||
|   bool get_supports_current_humidity() const { return this->supports_current_humidity_; } | ||||
|   ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") | ||||
|   bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); } | ||||
|   ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") | ||||
|   void set_supports_current_humidity(bool supports_current_humidity) { | ||||
|     this->supports_current_humidity_ = supports_current_humidity; | ||||
|     if (supports_current_humidity) { | ||||
|       this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); | ||||
|     } else { | ||||
|       this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); | ||||
|     } | ||||
|   } | ||||
|   bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; } | ||||
|   ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") | ||||
|   bool get_supports_two_point_target_temperature() const { | ||||
|     return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); | ||||
|   } | ||||
|   ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") | ||||
|   void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { | ||||
|     this->supports_two_point_target_temperature_ = supports_two_point_target_temperature; | ||||
|     if (supports_two_point_target_temperature) | ||||
|     // Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior | ||||
|     { | ||||
|       this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); | ||||
|     } else { | ||||
|       this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); | ||||
|     } | ||||
|   } | ||||
|   bool get_supports_target_humidity() const { return this->supports_target_humidity_; } | ||||
|   ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") | ||||
|   bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); } | ||||
|   ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") | ||||
|   void set_supports_target_humidity(bool supports_target_humidity) { | ||||
|     this->supports_target_humidity_ = supports_target_humidity; | ||||
|     if (supports_target_humidity) { | ||||
|       this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); | ||||
|     } else { | ||||
|       this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); | ||||
|     } | ||||
|   } | ||||
|   void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } | ||||
|   ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") | ||||
|   bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); } | ||||
|   ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") | ||||
|   void set_supports_action(bool supports_action) { | ||||
|     if (supports_action) { | ||||
|       this->add_feature_flags(CLIMATE_SUPPORTS_ACTION); | ||||
|     } else { | ||||
|       this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } | ||||
|   void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_fan_only_mode(bool supports_fan_only_mode) { | ||||
|     set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode); | ||||
|   } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||
|   void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } | ||||
|   bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } | ||||
|   const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } | ||||
|   const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } | ||||
|  | ||||
|   void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; } | ||||
|   bool get_supports_action() const { return this->supports_action_; } | ||||
|  | ||||
|   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } | ||||
|   void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } | ||||
|   void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } | ||||
|   void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||
|   void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } | ||||
|   void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } | ||||
|   bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } | ||||
|   bool get_supports_fan_modes() const { | ||||
|     return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); | ||||
|   } | ||||
|   const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|   const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|  | ||||
|   void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { | ||||
|   void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) { | ||||
|     this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); | ||||
|   } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) { | ||||
|     this->supported_custom_fan_modes_ = modes; | ||||
|   } | ||||
|   template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { | ||||
|     this->supported_custom_fan_modes_.assign(modes, modes + N); | ||||
|   } | ||||
|   const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { | ||||
|     return this->supported_custom_fan_modes_.count(custom_fan_mode); | ||||
|     return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); | ||||
|   } | ||||
|  | ||||
|   void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } | ||||
|   void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } | ||||
|   void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } | ||||
|   void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } | ||||
|   void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } | ||||
|   bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } | ||||
|   bool get_supports_presets() const { return !this->supported_presets_.empty(); } | ||||
|   const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } | ||||
|   const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } | ||||
|  | ||||
|   void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { | ||||
|   void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { | ||||
|     this->supported_custom_presets_ = std::move(supported_custom_presets); | ||||
|   } | ||||
|   const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   void set_supported_custom_presets(std::initializer_list<std::string> presets) { | ||||
|     this->supported_custom_presets_ = presets; | ||||
|   } | ||||
|   template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { | ||||
|     this->supported_custom_presets_.assign(presets, presets + N); | ||||
|   } | ||||
|   const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   bool supports_custom_preset(const std::string &custom_preset) const { | ||||
|     return this->supported_custom_presets_.count(custom_preset); | ||||
|     return vector_contains(this->supported_custom_presets_, custom_preset); | ||||
|   } | ||||
|  | ||||
|   void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } | ||||
|   void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } | ||||
|   void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") | ||||
|   void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") | ||||
|   void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") | ||||
|   void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") | ||||
|   void set_supports_swing_mode_horizontal(bool supported) { | ||||
|     set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); | ||||
|   } | ||||
|   bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } | ||||
|   bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|   const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|  | ||||
|   float get_visual_min_temperature() const { return this->visual_min_temperature_; } | ||||
|   void set_visual_min_temperature(float visual_min_temperature) { | ||||
| @@ -180,23 +205,6 @@ class ClimateTraits { | ||||
|   void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } | ||||
|  | ||||
|  protected: | ||||
| #ifdef USE_API | ||||
|   // The API connection is a friend class to access internal methods | ||||
|   friend class api::APIConnection; | ||||
|   // These methods return references to internal data structures. | ||||
|   // They are used by the API to avoid copying data when encoding messages. | ||||
|   // Warning: Do not use these methods outside of the API connection code. | ||||
|   // They return references to internal data that can be invalidated. | ||||
|   const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; } | ||||
|   const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const { | ||||
|     return this->supported_custom_fan_modes_; | ||||
|   } | ||||
|   const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; } | ||||
|   const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } | ||||
| #endif | ||||
|  | ||||
|   void set_mode_support_(climate::ClimateMode mode, bool supported) { | ||||
|     if (supported) { | ||||
|       this->supported_modes_.insert(mode); | ||||
| @@ -219,24 +227,20 @@ class ClimateTraits { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool supports_current_temperature_{false}; | ||||
|   bool supports_current_humidity_{false}; | ||||
|   bool supports_two_point_target_temperature_{false}; | ||||
|   bool supports_target_humidity_{false}; | ||||
|   std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; | ||||
|   bool supports_action_{false}; | ||||
|   std::set<climate::ClimateFanMode> supported_fan_modes_; | ||||
|   std::set<climate::ClimateSwingMode> supported_swing_modes_; | ||||
|   std::set<climate::ClimatePreset> supported_presets_; | ||||
|   std::set<std::string> supported_custom_fan_modes_; | ||||
|   std::set<std::string> supported_custom_presets_; | ||||
|  | ||||
|   uint32_t feature_flags_{0}; | ||||
|   float visual_min_temperature_{10}; | ||||
|   float visual_max_temperature_{30}; | ||||
|   float visual_target_temperature_step_{0.1}; | ||||
|   float visual_current_temperature_step_{0.1}; | ||||
|   float visual_min_humidity_{30}; | ||||
|   float visual_max_humidity_{99}; | ||||
|  | ||||
|   climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; | ||||
|   climate::ClimateFanModeMask supported_fan_modes_; | ||||
|   climate::ClimateSwingModeMask supported_swing_modes_; | ||||
|   climate::ClimatePresetMask supported_presets_; | ||||
|   std::vector<std::string> supported_custom_fan_modes_; | ||||
|   std::vector<std::string> supported_custom_presets_; | ||||
| }; | ||||
|  | ||||
| }  // namespace climate | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import core | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import climate, remote_base, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| @@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema( | ||||
|     ) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| def deprecated_schema_constant(config): | ||||
|     type: str = "unknown" | ||||
|     if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): | ||||
|         type = str(id.type).split("::", maxsplit=1)[0] | ||||
|     _LOGGER.warning( | ||||
|         "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " | ||||
|         "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " | ||||
|         "If you are seeing this, report an issue to the external_component author and ask them to update it. " | ||||
|         "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " | ||||
|         "Component using this schema: %s", | ||||
|         type, | ||||
|     ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) | ||||
| CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) | ||||
|  | ||||
|  | ||||
| async def register_climate_ir(var, config): | ||||
|     await cg.register_component(var, config) | ||||
|     await remote_base.register_transmittable(var, config) | ||||
|   | ||||
| @@ -8,7 +8,10 @@ static const char *const TAG = "climate_ir"; | ||||
|  | ||||
| climate::ClimateTraits ClimateIR::traits() { | ||||
|   auto traits = climate::ClimateTraits(); | ||||
|   traits.set_supports_current_temperature(this->sensor_ != nullptr); | ||||
|   if (this->sensor_ != nullptr) { | ||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); | ||||
|   } | ||||
|  | ||||
|   traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); | ||||
|   if (this->supports_cool_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||
| @@ -19,7 +22,6 @@ climate::ClimateTraits ClimateIR::traits() { | ||||
|   if (this->supports_fan_only_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); | ||||
|  | ||||
|   traits.set_supports_two_point_target_temperature(false); | ||||
|   traits.set_visual_min_temperature(this->minimum_temperature_); | ||||
|   traits.set_visual_max_temperature(this->maximum_temperature_); | ||||
|   traits.set_visual_temperature_step(this->temperature_step_); | ||||
|   | ||||
| @@ -24,16 +24,18 @@ class ClimateIR : public Component, | ||||
|                   public remote_base::RemoteTransmittable { | ||||
|  public: | ||||
|   ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, | ||||
|             bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, | ||||
|             std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { | ||||
|             bool supports_dry = false, bool supports_fan_only = false, | ||||
|             climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), | ||||
|             climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), | ||||
|             climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { | ||||
|     this->minimum_temperature_ = minimum_temperature; | ||||
|     this->maximum_temperature_ = maximum_temperature; | ||||
|     this->temperature_step_ = temperature_step; | ||||
|     this->supports_dry_ = supports_dry; | ||||
|     this->supports_fan_only_ = supports_fan_only; | ||||
|     this->fan_modes_ = std::move(fan_modes); | ||||
|     this->swing_modes_ = std::move(swing_modes); | ||||
|     this->presets_ = std::move(presets); | ||||
|     this->fan_modes_ = fan_modes; | ||||
|     this->swing_modes_ = swing_modes; | ||||
|     this->presets_ = presets; | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
| @@ -60,9 +62,9 @@ class ClimateIR : public Component, | ||||
|   bool supports_heat_{true}; | ||||
|   bool supports_dry_{false}; | ||||
|   bool supports_fan_only_{false}; | ||||
|   std::set<climate::ClimateFanMode> fan_modes_ = {}; | ||||
|   std::set<climate::ClimateSwingMode> swing_modes_ = {}; | ||||
|   std::set<climate::ClimatePreset> presets_ = {}; | ||||
|   climate::ClimateFanModeMask fan_modes_{}; | ||||
|   climate::ClimateSwingModeMask swing_modes_{}; | ||||
|   climate::ClimatePresetMask presets_{}; | ||||
|  | ||||
|   sensor::Sensor *sensor_{nullptr}; | ||||
| }; | ||||
|   | ||||
| @@ -151,11 +151,6 @@ def cover_schema( | ||||
|     return _COVER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| COVER_SCHEMA = cover_schema(Cover) | ||||
| COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) | ||||
|  | ||||
|  | ||||
| async def setup_cover_core_(var, config): | ||||
|     await setup_entity(var, config, "cover") | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #include "cover.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <strings.h> | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cover { | ||||
| @@ -144,21 +144,7 @@ CoverCall &CoverCall::set_stop(bool stop) { | ||||
| bool CoverCall::get_stop() const { return this->stop_; } | ||||
|  | ||||
| CoverCall Cover::make_call() { return {this}; } | ||||
| void Cover::open() { | ||||
|   auto call = this->make_call(); | ||||
|   call.set_command_open(); | ||||
|   call.perform(); | ||||
| } | ||||
| void Cover::close() { | ||||
|   auto call = this->make_call(); | ||||
|   call.set_command_close(); | ||||
|   call.perform(); | ||||
| } | ||||
| void Cover::stop() { | ||||
|   auto call = this->make_call(); | ||||
|   call.set_command_stop(); | ||||
|   call.perform(); | ||||
| } | ||||
|  | ||||
| void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); } | ||||
| void Cover::publish_state(bool save) { | ||||
|   this->position = clamp(this->position, 0.0f, 1.0f); | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/core/entity_base.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| #include "cover_traits.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -125,25 +126,6 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { | ||||
|  | ||||
|   /// Construct a new cover call used to control the cover. | ||||
|   CoverCall make_call(); | ||||
|   /** Open the cover. | ||||
|    * | ||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||
|    */ | ||||
|   ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9") | ||||
|   void open(); | ||||
|   /** Close the cover. | ||||
|    * | ||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||
|    */ | ||||
|   ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9") | ||||
|   void close(); | ||||
|   /** Stop the cover. | ||||
|    * | ||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||
|    * As per solution from issue #2885 the call should include perform() | ||||
|    */ | ||||
|   ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9") | ||||
|   void stop(); | ||||
|  | ||||
|   void add_on_state_callback(std::function<void()> &&f); | ||||
|  | ||||
|   | ||||
| @@ -241,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() { | ||||
|  | ||||
| climate::ClimateTraits DaikinArcClimate::traits() { | ||||
|   climate::ClimateTraits traits = climate_ir::ClimateIR::traits(); | ||||
|   traits.set_supports_current_temperature(true); | ||||
|   traits.set_supports_current_humidity(false); | ||||
|   traits.set_supports_target_humidity(true); | ||||
|   traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY); | ||||
|   traits.set_visual_min_humidity(38); | ||||
|   traits.set_visual_max_humidity(52); | ||||
|   return traits; | ||||
|   | ||||
| @@ -11,8 +11,6 @@ | ||||
| #include <esp_chip_info.h> | ||||
| #include <esp_partition.h> | ||||
|  | ||||
| #include <map> | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #include <Esp.h> | ||||
| #endif | ||||
| @@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() { | ||||
|  | ||||
| uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); } | ||||
|  | ||||
| static const std::map<int, const char *> CHIP_FEATURES = { | ||||
| struct ChipFeature { | ||||
|   int bit; | ||||
|   const char *name; | ||||
| }; | ||||
|  | ||||
| static constexpr ChipFeature CHIP_FEATURES[] = { | ||||
|     {CHIP_FEATURE_BLE, "BLE"}, | ||||
|     {CHIP_FEATURE_BT, "BT"}, | ||||
|     {CHIP_FEATURE_EMB_FLASH, "EMB Flash"}, | ||||
| @@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) { | ||||
|   esp_chip_info(&info); | ||||
|   const char *model = ESPHOME_VARIANT; | ||||
|   std::string features; | ||||
|   for (auto feature : CHIP_FEATURES) { | ||||
|     if (info.features & feature.first) { | ||||
|       features += feature.second; | ||||
|  | ||||
|   // Check each known feature bit | ||||
|   for (const auto &feature : CHIP_FEATURES) { | ||||
|     if (info.features & feature.bit) { | ||||
|       features += feature.name; | ||||
|       features += ", "; | ||||
|       info.features &= ~feature.first; | ||||
|       info.features &= ~feature.bit; | ||||
|     } | ||||
|   } | ||||
|   if (info.features != 0) | ||||
|   | ||||
| @@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r | ||||
|   reset_reason += reason; | ||||
| } | ||||
|  | ||||
| inline uint32_t read_mem_u32(uintptr_t addr) { | ||||
| static inline uint32_t read_mem_u32(uintptr_t addr) { | ||||
|   return *reinterpret_cast<volatile uint32_t *>(addr);  // NOLINT(performance-no-int-to-ptr) | ||||
| } | ||||
|  | ||||
| static inline uint8_t read_mem_u8(uintptr_t addr) { | ||||
|   return *reinterpret_cast<volatile uint8_t *>(addr);  // NOLINT(performance-no-int-to-ptr) | ||||
| } | ||||
|  | ||||
| // defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information | ||||
| constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB; | ||||
| constexpr uintptr_t MBR_SIZE = 0x1000; | ||||
| constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000; | ||||
| constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10; | ||||
| constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14; | ||||
|  | ||||
| static inline bool is_sd_present() { | ||||
|   return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER; | ||||
| } | ||||
| static inline uint32_t sd_id_get() { | ||||
|   if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) { | ||||
|     return read_mem_u32(MBR_SIZE + SD_ID_OFFSET); | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
| static inline uint32_t sd_version_get() { | ||||
|   if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) { | ||||
|     return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET); | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| std::string DebugComponent::get_reset_reason_() { | ||||
|   uint32_t cause; | ||||
|   auto ret = hwinfo_get_reset_cause(&cause); | ||||
| @@ -271,6 +298,29 @@ void DebugComponent::get_device_info_(std::string &device_info) { | ||||
|            NRF_UICR->NRFFW[0]); | ||||
|   ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), | ||||
|            NRF_UICR->NRFFW[1]); | ||||
|   if (is_sd_present()) { | ||||
|     uint32_t const sd_id = sd_id_get(); | ||||
|     uint32_t const sd_version = sd_version_get(); | ||||
|  | ||||
|     uint32_t ver[3]; | ||||
|     ver[0] = sd_version / 1000000; | ||||
|     ver[1] = (sd_version - ver[0] * 1000000) / 1000; | ||||
|     ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); | ||||
|  | ||||
|     ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); | ||||
| #ifdef USE_SOFTDEVICE_ID | ||||
| #ifdef USE_SOFTDEVICE_VERSION | ||||
|     if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { | ||||
|       ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.", | ||||
|                USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION); | ||||
|     } | ||||
| #else | ||||
|     if (USE_SOFTDEVICE_ID != sd_id) { | ||||
|       ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID); | ||||
|     } | ||||
| #endif | ||||
| #endif | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -82,16 +82,14 @@ class DemoClimate : public climate::Climate, public Component { | ||||
|     climate::ClimateTraits traits{}; | ||||
|     switch (type_) { | ||||
|       case DemoClimateType::TYPE_1: | ||||
|         traits.set_supports_current_temperature(true); | ||||
|         traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); | ||||
|         traits.set_supported_modes({ | ||||
|             climate::CLIMATE_MODE_OFF, | ||||
|             climate::CLIMATE_MODE_HEAT, | ||||
|         }); | ||||
|         traits.set_supports_action(true); | ||||
|         traits.set_visual_temperature_step(0.5); | ||||
|         break; | ||||
|       case DemoClimateType::TYPE_2: | ||||
|         traits.set_supports_current_temperature(false); | ||||
|         traits.set_supported_modes({ | ||||
|             climate::CLIMATE_MODE_OFF, | ||||
|             climate::CLIMATE_MODE_HEAT, | ||||
| @@ -100,7 +98,7 @@ class DemoClimate : public climate::Climate, public Component { | ||||
|             climate::CLIMATE_MODE_DRY, | ||||
|             climate::CLIMATE_MODE_FAN_ONLY, | ||||
|         }); | ||||
|         traits.set_supports_action(true); | ||||
|         traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); | ||||
|         traits.set_supported_fan_modes({ | ||||
|             climate::CLIMATE_FAN_ON, | ||||
|             climate::CLIMATE_FAN_OFF, | ||||
| @@ -123,8 +121,8 @@ class DemoClimate : public climate::Climate, public Component { | ||||
|         traits.set_supported_custom_presets({"My Preset"}); | ||||
|         break; | ||||
|       case DemoClimateType::TYPE_3: | ||||
|         traits.set_supports_current_temperature(true); | ||||
|         traits.set_supports_two_point_target_temperature(true); | ||||
|         traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | | ||||
|                                  climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); | ||||
|         traits.set_supported_modes({ | ||||
|             climate::CLIMATE_MODE_OFF, | ||||
|             climate::CLIMATE_MODE_COOL, | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| #include "e131_addressable_light_effect.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| @@ -76,14 +78,14 @@ void E131Component::loop() { | ||||
| } | ||||
|  | ||||
| void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
|   if (light_effects_.count(light_effect)) { | ||||
|   if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.insert(light_effect); | ||||
|   light_effects_.push_back(light_effect); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     join_(universe); | ||||
| @@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
| } | ||||
|  | ||||
| void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { | ||||
|   if (!light_effects_.count(light_effect)) { | ||||
|   auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); | ||||
|   if (it == light_effects_.end()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.erase(light_effect); | ||||
|   // Swap with last element and pop for O(1) removal (order doesn't matter) | ||||
|   *it = light_effects_.back(); | ||||
|   light_effects_.pop_back(); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     leave_(universe); | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
| #include <cinttypes> | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -47,9 +46,8 @@ class E131Component : public esphome::Component { | ||||
|  | ||||
|   E131ListenMethod listen_method_{E131_MULTICAST}; | ||||
|   std::unique_ptr<socket::Socket> socket_; | ||||
|   std::set<E131AddressableLightEffect *> light_effects_; | ||||
|   std::vector<E131AddressableLightEffect *> light_effects_; | ||||
|   std::map<int, int> universe_consumers_; | ||||
|   std::map<int, E131Packet> universe_packets_; | ||||
| }; | ||||
|  | ||||
| }  // namespace e131 | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace e131 { | ||||
| static const char *const TAG = "e131_addressable_light_effect"; | ||||
| static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); | ||||
|  | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } | ||||
|  | ||||
| @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet | ||||
|       std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); | ||||
|   auto *input_data = packet.values + 1; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, | ||||
|            output_offset, output_end); | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, | ||||
|            output_end); | ||||
|  | ||||
|   switch (channels_) { | ||||
|     case E131_MONO: | ||||
|   | ||||
| @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; | ||||
|  | ||||
| class E131AddressableLightEffect : public light::AddressableLightEffect { | ||||
|  public: | ||||
|   E131AddressableLightEffect(const std::string &name); | ||||
|   E131AddressableLightEffect(const char *name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() { | ||||
|   if (this->busy_pin_ == nullptr) { | ||||
|     return true; | ||||
|   } | ||||
|   return !this->busy_pin_->digital_read(); | ||||
|   return this->busy_pin_->digital_read(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::reset() { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import contextlib | ||||
| from dataclasses import dataclass | ||||
| import itertools | ||||
| import logging | ||||
| @@ -102,6 +103,10 @@ COMPILER_OPTIMIZATIONS = { | ||||
|     "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", | ||||
| } | ||||
|  | ||||
| # Socket limit configuration for ESP-IDF | ||||
| # ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10 | ||||
| DEFAULT_MAX_SOCKETS = 10  # ESP-IDF default | ||||
|  | ||||
| ARDUINO_ALLOWED_VARIANTS = [ | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
| @@ -299,9 +304,24 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||
| def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: | ||||
|     # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to | ||||
|     # a PIO platformio/framework-espidf value | ||||
|     if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): | ||||
|         ext = "tar.xz" | ||||
|     else: | ||||
|         ext = "zip" | ||||
|     if release: | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" | ||||
|  | ||||
|  | ||||
| def _is_framework_url(source: str) -> str: | ||||
|     # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink | ||||
|     import urllib.parse | ||||
|  | ||||
|     try: | ||||
|         parsed = urllib.parse.urlparse(source) | ||||
|     except ValueError: | ||||
|         return False | ||||
|     return bool(parsed.scheme) | ||||
|  | ||||
|  | ||||
| # NOTE: Keep this in mind when updating the recommended version: | ||||
| @@ -313,7 +333,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: | ||||
| # The default/recommended arduino framework version | ||||
| #  - https://github.com/espressif/arduino-esp32/releases | ||||
| ARDUINO_FRAMEWORK_VERSION_LOOKUP = { | ||||
|     "recommended": cv.Version(3, 2, 1), | ||||
|     "recommended": cv.Version(3, 3, 2), | ||||
|     "latest": cv.Version(3, 3, 2), | ||||
|     "dev": cv.Version(3, 3, 2), | ||||
| } | ||||
| @@ -332,13 +352,14 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { | ||||
| # The default/recommended esp-idf framework version | ||||
| #  - https://github.com/espressif/esp-idf/releases | ||||
| ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { | ||||
|     "recommended": cv.Version(5, 4, 2), | ||||
|     "recommended": cv.Version(5, 5, 1), | ||||
|     "latest": cv.Version(5, 5, 1), | ||||
|     "dev": cv.Version(5, 5, 1), | ||||
| } | ||||
| ESP_IDF_PLATFORM_VERSION_LOOKUP = { | ||||
|     cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 4, 3): cv.Version(55, 3, 32), | ||||
|     cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), | ||||
| @@ -352,7 +373,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { | ||||
| # The platform-espressif32 version | ||||
| #  - https://github.com/pioarduino/platform-espressif32/releases | ||||
| PLATFORM_VERSION_LOOKUP = { | ||||
|     "recommended": cv.Version(54, 3, 21, "2"), | ||||
|     "recommended": cv.Version(55, 3, 31, "1"), | ||||
|     "latest": cv.Version(55, 3, 31, "1"), | ||||
|     "dev": cv.Version(55, 3, 31, "1"), | ||||
| } | ||||
| @@ -387,7 +408,7 @@ def _check_versions(value): | ||||
|         value[CONF_SOURCE] = value.get( | ||||
|             CONF_SOURCE, _format_framework_arduino_version(version) | ||||
|         ) | ||||
|         if value[CONF_SOURCE].startswith("http"): | ||||
|         if _is_framework_url(value[CONF_SOURCE]): | ||||
|             value[CONF_SOURCE] = ( | ||||
|                 f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" | ||||
|             ) | ||||
| @@ -400,7 +421,7 @@ def _check_versions(value): | ||||
|             CONF_SOURCE, | ||||
|             _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), | ||||
|         ) | ||||
|         if value[CONF_SOURCE].startswith("http"): | ||||
|         if _is_framework_url(value[CONF_SOURCE]): | ||||
|             value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" | ||||
|  | ||||
|     if CONF_PLATFORM_VERSION not in value: | ||||
| @@ -533,6 +554,33 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | ||||
| CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||
| CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | ||||
| CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | ||||
| CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | ||||
| CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" | ||||
| CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" | ||||
| CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" | ||||
|  | ||||
| # VFS requirement tracking | ||||
| # Components that need VFS features can call require_vfs_select() or require_vfs_dir() | ||||
| KEY_VFS_SELECT_REQUIRED = "vfs_select_required" | ||||
| KEY_VFS_DIR_REQUIRED = "vfs_dir_required" | ||||
|  | ||||
|  | ||||
| def require_vfs_select() -> None: | ||||
|     """Mark that VFS select support is required by a component. | ||||
|  | ||||
|     Call this from components that use esp_vfs_eventfd or other VFS select features. | ||||
|     This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled. | ||||
|     """ | ||||
|     CORE.data[KEY_VFS_SELECT_REQUIRED] = True | ||||
|  | ||||
|  | ||||
| def require_vfs_dir() -> None: | ||||
|     """Mark that VFS directory support is required by a component. | ||||
|  | ||||
|     Call this from components that use directory functions (opendir, readdir, mkdir, etc.). | ||||
|     This prevents CONFIG_VFS_SUPPORT_DIR from being disabled. | ||||
|     """ | ||||
|     CORE.data[KEY_VFS_DIR_REQUIRED] = True | ||||
|  | ||||
|  | ||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||
| @@ -595,6 +643,16 @@ FRAMEWORK_SCHEMA = cv.All( | ||||
|                     cv.Optional( | ||||
|                         CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_VFS_SUPPORT_SELECT, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, | ||||
|                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
| @@ -731,6 +789,72 @@ CONFIG_SCHEMA = cv.All( | ||||
| FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) | ||||
|  | ||||
|  | ||||
| def _configure_lwip_max_sockets(conf: dict) -> None: | ||||
|     """Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs. | ||||
|  | ||||
|     Socket component tracks consumer needs via consume_sockets() called during config validation. | ||||
|     This function runs in to_code() after all components have registered their socket needs. | ||||
|     User-provided sdkconfig_options take precedence. | ||||
|     """ | ||||
|     from esphome.components.socket import KEY_SOCKET_CONSUMERS | ||||
|  | ||||
|     # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS | ||||
|     user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get( | ||||
|         "CONFIG_LWIP_MAX_SOCKETS" | ||||
|     ) | ||||
|  | ||||
|     socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) | ||||
|     total_sockets = sum(socket_consumers.values()) | ||||
|  | ||||
|     # Early return if no sockets registered and no user override | ||||
|     if total_sockets == 0 and user_max_sockets is None: | ||||
|         return | ||||
|  | ||||
|     components_list = ", ".join( | ||||
|         f"{name}={count}" for name, count in sorted(socket_consumers.items()) | ||||
|     ) | ||||
|  | ||||
|     # User specified their own value - respect it but warn if insufficient | ||||
|     if user_max_sockets is not None: | ||||
|         _LOGGER.info( | ||||
|             "Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s", | ||||
|             user_max_sockets, | ||||
|         ) | ||||
|  | ||||
|         # Warn if user's value is less than what components need | ||||
|         if total_sockets > 0: | ||||
|             user_sockets_int = 0 | ||||
|             with contextlib.suppress(ValueError, TypeError): | ||||
|                 user_sockets_int = int(user_max_sockets) | ||||
|  | ||||
|             if user_sockets_int < total_sockets: | ||||
|                 _LOGGER.warning( | ||||
|                     "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " | ||||
|                     "needs %d sockets (registered: %s). You may experience socket " | ||||
|                     "exhaustion errors. Consider increasing to at least %d.", | ||||
|                     user_sockets_int, | ||||
|                     total_sockets, | ||||
|                     components_list, | ||||
|                     total_sockets, | ||||
|                 ) | ||||
|         # User's value already added via sdkconfig_options processing | ||||
|         return | ||||
|  | ||||
|     # Auto-calculate based on component needs | ||||
|     # Use at least the ESP-IDF default (10), or the total needed by components | ||||
|     max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) | ||||
|  | ||||
|     log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG | ||||
|     _LOGGER.log( | ||||
|         log_level, | ||||
|         "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", | ||||
|         max_sockets, | ||||
|         components_list, | ||||
|     ) | ||||
|  | ||||
|     add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) | ||||
| @@ -758,12 +882,27 @@ async def to_code(config): | ||||
|     for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): | ||||
|         os.environ.pop(clean_var, None) | ||||
|  | ||||
|     # Set the location of the IDF component manager cache | ||||
|     os.environ["IDF_COMPONENT_CACHE_PATH"] = str( | ||||
|         CORE.relative_internal_path(".espressif") | ||||
|     ) | ||||
|  | ||||
|     add_extra_script( | ||||
|         "post", | ||||
|         "post_build.py", | ||||
|         Path(__file__).parent / "post_build.py.script", | ||||
|     ) | ||||
|  | ||||
|     # In testing mode, add IRAM fix script to allow linking grouped component tests | ||||
|     # Similar to ESP8266's approach but for ESP-IDF | ||||
|     if CORE.testing_mode: | ||||
|         cg.add_build_flag("-DESPHOME_TESTING_MODE") | ||||
|         add_extra_script( | ||||
|             "pre", | ||||
|             "iram_fix.py", | ||||
|             Path(__file__).parent / "iram_fix.py.script", | ||||
|         ) | ||||
|  | ||||
|     if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: | ||||
|         cg.add_platformio_option("framework", "espidf") | ||||
|         cg.add_build_flag("-DUSE_ESP_IDF") | ||||
| @@ -814,6 +953,9 @@ async def to_code(config): | ||||
|     # Disable dynamic log level control to save memory | ||||
|     add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) | ||||
|  | ||||
|     # Reduce PHY TX power in the event of a brownout | ||||
|     add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) | ||||
|  | ||||
|     # Set default CPU frequency | ||||
|     add_idf_sdkconfig_option( | ||||
|         f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True | ||||
| @@ -838,6 +980,9 @@ async def to_code(config): | ||||
|         add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) | ||||
|     if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): | ||||
|         add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) | ||||
|  | ||||
|     _configure_lwip_max_sockets(conf) | ||||
|  | ||||
|     if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): | ||||
|         add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) | ||||
|         add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) | ||||
| @@ -854,6 +999,49 @@ async def to_code(config): | ||||
|     if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): | ||||
|         add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) | ||||
|  | ||||
|     # Disable placing libc locks in IRAM to save RAM | ||||
|     # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) | ||||
|     # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. | ||||
|     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): | ||||
|         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) | ||||
|  | ||||
|     # Disable VFS support for termios (terminal I/O functions) | ||||
|     # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). | ||||
|     # Saves approximately 1.8KB of flash when disabled (default). | ||||
|     add_idf_sdkconfig_option( | ||||
|         "CONFIG_VFS_SUPPORT_TERMIOS", | ||||
|         not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), | ||||
|     ) | ||||
|  | ||||
|     # Disable VFS support for select() with file descriptors | ||||
|     # ESPHome only uses select() with sockets via lwip_select(), which still works. | ||||
|     # VFS select is only needed for UART/eventfd file descriptors. | ||||
|     # Components that need it (e.g., openthread) call require_vfs_select(). | ||||
|     # Saves approximately 2.7KB of flash when disabled (default). | ||||
|     if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False): | ||||
|         # Component requires VFS select - force enable regardless of user setting | ||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True) | ||||
|     else: | ||||
|         # No component needs it - allow user to control (default: disabled) | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_VFS_SUPPORT_SELECT", | ||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), | ||||
|         ) | ||||
|  | ||||
|     # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) | ||||
|     # ESPHome doesn't use directory functions on ESP32. | ||||
|     # Components that need it (e.g., storage components) call require_vfs_dir(). | ||||
|     # Saves approximately 0.5KB+ of flash when disabled (default). | ||||
|     if CORE.data.get(KEY_VFS_DIR_REQUIRED, False): | ||||
|         # Component requires VFS directory support - force enable regardless of user setting | ||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True) | ||||
|     else: | ||||
|         # No component needs it - allow user to control (default: disabled) | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_VFS_SUPPORT_DIR", | ||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), | ||||
|         ) | ||||
|  | ||||
|     cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||
|     if CONF_PARTITIONS in config: | ||||
|         add_extra_build_file( | ||||
|   | ||||
| @@ -1564,6 +1564,10 @@ BOARDS = { | ||||
|         "name": "DFRobot Beetle ESP32-C3", | ||||
|         "variant": VARIANT_ESP32C3, | ||||
|     }, | ||||
|     "dfrobot_firebeetle2_esp32c6": { | ||||
|         "name": "DFRobot FireBeetle 2 ESP32-C6", | ||||
|         "variant": VARIANT_ESP32C6, | ||||
|     }, | ||||
|     "dfrobot_firebeetle2_esp32e": { | ||||
|         "name": "DFRobot Firebeetle 2 ESP32-E", | ||||
|         "variant": VARIANT_ESP32, | ||||
| @@ -1604,6 +1608,22 @@ BOARDS = { | ||||
|         "name": "Ai-Thinker ESP-C3-M1-I-Kit", | ||||
|         "variant": VARIANT_ESP32C3, | ||||
|     }, | ||||
|     "esp32-c5-devkitc-1": { | ||||
|         "name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM", | ||||
|         "variant": VARIANT_ESP32C5, | ||||
|     }, | ||||
|     "esp32-c5-devkitc1-n16r4": { | ||||
|         "name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)", | ||||
|         "variant": VARIANT_ESP32C5, | ||||
|     }, | ||||
|     "esp32-c5-devkitc1-n4": { | ||||
|         "name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)", | ||||
|         "variant": VARIANT_ESP32C5, | ||||
|     }, | ||||
|     "esp32-c5-devkitc1-n8r4": { | ||||
|         "name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)", | ||||
|         "variant": VARIANT_ESP32C5, | ||||
|     }, | ||||
|     "esp32-c6-devkitc-1": { | ||||
|         "name": "Espressif ESP32-C6-DevKitC-1", | ||||
|         "variant": VARIANT_ESP32C6, | ||||
| @@ -2048,6 +2068,10 @@ BOARDS = { | ||||
|         "name": "M5Stack Station", | ||||
|         "variant": VARIANT_ESP32, | ||||
|     }, | ||||
|     "m5stack-tab5-p4": { | ||||
|         "name": "M5STACK Tab5 esp32-p4 Board", | ||||
|         "variant": VARIANT_ESP32P4, | ||||
|     }, | ||||
|     "m5stack-timer-cam": { | ||||
|         "name": "M5Stack Timer CAM", | ||||
|         "variant": VARIANT_ESP32, | ||||
| @@ -2476,6 +2500,10 @@ BOARDS = { | ||||
|         "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", | ||||
|         "variant": VARIANT_ESP32S3, | ||||
|     }, | ||||
|     "yb_esp32s3_drv": { | ||||
|         "name": "YelloByte YB-ESP32-S3-DRV", | ||||
|         "variant": VARIANT_ESP32S3, | ||||
|     }, | ||||
|     "yb_esp32s3_eth": { | ||||
|         "name": "YelloByte YB-ESP32-S3-ETH", | ||||
|         "variant": VARIANT_ESP32S3, | ||||
|   | ||||
| @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   // - 3 bytes for members below | ||||
|   // - 1 byte padding for alignment | ||||
|   // - 4 bytes for vtable pointer | ||||
|   uint8_t pin_;        // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_;  // GPIO flags (1 byte) | ||||
|   uint8_t pin_;          // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_{};  // GPIO flags (1 byte) | ||||
|   struct PinFlags { | ||||
|     uint8_t inverted : 1;        // Invert pin logic (1 bit) | ||||
|     uint8_t drive_strength : 2;  // Drive strength 0-3 (2 bits) | ||||
|     uint8_t reserved : 5;        // Reserved for future use (5 bits) | ||||
|   } pin_flags_;                  // Total: 1 byte | ||||
|   } pin_flags_{};                // Total: 1 byte | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     if CONF_DRIVE_STRENGTH in config: | ||||
|         cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|   | ||||
							
								
								
									
										71
									
								
								esphome/components/esp32/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								esphome/components/esp32/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import os | ||||
| import re | ||||
|  | ||||
| # pylint: disable=E0602 | ||||
| Import("env")  # noqa | ||||
|  | ||||
| # IRAM size for testing mode (2MB - large enough to accommodate grouped tests) | ||||
| TESTING_IRAM_SIZE = 0x200000 | ||||
|  | ||||
|  | ||||
| def patch_idf_linker_script(source, target, env): | ||||
|     """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" | ||||
|     # Check if we're in testing mode by looking for the define | ||||
|     build_flags = env.get("BUILD_FLAGS", []) | ||||
|     testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) | ||||
|  | ||||
|     if not testing_mode: | ||||
|         return | ||||
|  | ||||
|     # For ESP-IDF, the linker scripts are generated in the build directory | ||||
|     build_dir = env.subst("$BUILD_DIR") | ||||
|  | ||||
|     # The memory.ld file is directly in the build directory | ||||
|     memory_ld = os.path.join(build_dir, "memory.ld") | ||||
|  | ||||
|     if not os.path.exists(memory_ld): | ||||
|         print(f"ESPHome: Warning - could not find linker script at {memory_ld}") | ||||
|         return | ||||
|  | ||||
|     try: | ||||
|         with open(memory_ld, "r") as f: | ||||
|             content = f.read() | ||||
|     except OSError as e: | ||||
|         print(f"ESPHome: Error reading linker script: {e}") | ||||
|         return | ||||
|  | ||||
|     # Check if this file contains iram0_0_seg | ||||
|     if 'iram0_0_seg' not in content: | ||||
|         print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") | ||||
|         return | ||||
|  | ||||
|     # Look for iram0_0_seg definition and increase its length | ||||
|     # ESP-IDF format can be: | ||||
|     #   iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 | ||||
|     # or more complex with nested parentheses: | ||||
|     #   iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) | ||||
|     # We want to change len to TESTING_IRAM_SIZE for testing | ||||
|  | ||||
|     # Use a more robust approach: find the line and manually parse it | ||||
|     lines = content.split('\n') | ||||
|     for i, line in enumerate(lines): | ||||
|         if 'iram0_0_seg' in line and 'len' in line: | ||||
|             # Find the position of "len = " and replace everything after it until the end of the statement | ||||
|             match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) | ||||
|             if match: | ||||
|                 lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" | ||||
|                 break | ||||
|  | ||||
|     updated = '\n'.join(lines) | ||||
|  | ||||
|     if updated != content: | ||||
|         with open(memory_ld, "w") as f: | ||||
|             f.write(updated) | ||||
|         print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") | ||||
|     else: | ||||
|         print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") | ||||
|  | ||||
|  | ||||
| # Hook into the build process before linking | ||||
| # For ESP-IDF, we need to run this after the linker scripts are generated | ||||
| env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script) | ||||
| @@ -1,4 +1,5 @@ | ||||
| from collections.abc import Callable, MutableMapping | ||||
| from dataclasses import dataclass | ||||
| from enum import Enum | ||||
| import logging | ||||
| import re | ||||
| @@ -16,7 +17,7 @@ from esphome.const import ( | ||||
|     CONF_NAME, | ||||
|     CONF_NAME_ADD_MAC_SUFFIX, | ||||
| ) | ||||
| from esphome.core import CORE, TimePeriod | ||||
| from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
| @@ -107,8 +108,65 @@ class BTLoggers(Enum): | ||||
|     """ESP32 WiFi provisioning over Bluetooth""" | ||||
|  | ||||
|  | ||||
| # Set to track which loggers are needed by components | ||||
| _required_loggers: set[BTLoggers] = set() | ||||
| # Key for storing required loggers in CORE.data | ||||
| ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers" | ||||
|  | ||||
|  | ||||
| def _get_required_loggers() -> set[BTLoggers]: | ||||
|     """Get the set of required Bluetooth loggers from CORE.data.""" | ||||
|     return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set()) | ||||
|  | ||||
|  | ||||
| # Dataclass for handler registration counts | ||||
| @dataclass | ||||
| class HandlerCounts: | ||||
|     gap_event: int = 0 | ||||
|     gap_scan_event: int = 0 | ||||
|     gattc_event: int = 0 | ||||
|     gatts_event: int = 0 | ||||
|     ble_status_event: int = 0 | ||||
|  | ||||
|  | ||||
| # Track handler registration counts for StaticVector sizing | ||||
| _handler_counts = HandlerCounts() | ||||
|  | ||||
|  | ||||
| def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: | ||||
|     """Register a GAP event handler and track the count.""" | ||||
|     _handler_counts.gap_event += 1 | ||||
|     cg.add(parent_var.register_gap_event_handler(handler_var)) | ||||
|  | ||||
|  | ||||
| def register_gap_scan_event_handler( | ||||
|     parent_var: cg.MockObj, handler_var: cg.MockObj | ||||
| ) -> None: | ||||
|     """Register a GAP scan event handler and track the count.""" | ||||
|     _handler_counts.gap_scan_event += 1 | ||||
|     cg.add(parent_var.register_gap_scan_event_handler(handler_var)) | ||||
|  | ||||
|  | ||||
| def register_gattc_event_handler( | ||||
|     parent_var: cg.MockObj, handler_var: cg.MockObj | ||||
| ) -> None: | ||||
|     """Register a GATTc event handler and track the count.""" | ||||
|     _handler_counts.gattc_event += 1 | ||||
|     cg.add(parent_var.register_gattc_event_handler(handler_var)) | ||||
|  | ||||
|  | ||||
| def register_gatts_event_handler( | ||||
|     parent_var: cg.MockObj, handler_var: cg.MockObj | ||||
| ) -> None: | ||||
|     """Register a GATTs event handler and track the count.""" | ||||
|     _handler_counts.gatts_event += 1 | ||||
|     cg.add(parent_var.register_gatts_event_handler(handler_var)) | ||||
|  | ||||
|  | ||||
| def register_ble_status_event_handler( | ||||
|     parent_var: cg.MockObj, handler_var: cg.MockObj | ||||
| ) -> None: | ||||
|     """Register a BLE status event handler and track the count.""" | ||||
|     _handler_counts.ble_status_event += 1 | ||||
|     cg.add(parent_var.register_ble_status_event_handler(handler_var)) | ||||
|  | ||||
|  | ||||
| def register_bt_logger(*loggers: BTLoggers) -> None: | ||||
| @@ -117,12 +175,13 @@ def register_bt_logger(*loggers: BTLoggers) -> None: | ||||
|     Args: | ||||
|         *loggers: One or more BTLoggers enum members | ||||
|     """ | ||||
|     required_loggers = _get_required_loggers() | ||||
|     for logger in loggers: | ||||
|         if not isinstance(logger, BTLoggers): | ||||
|             raise TypeError( | ||||
|                 f"Logger must be a BTLoggers enum member, got {type(logger)}" | ||||
|             ) | ||||
|         _required_loggers.add(logger) | ||||
|         required_loggers.add(logger) | ||||
|  | ||||
|  | ||||
| CONF_BLE_ID = "ble_id" | ||||
| @@ -334,6 +393,15 @@ def final_validation(config): | ||||
|     max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) | ||||
|     validate_connection_slots(max_connections) | ||||
|  | ||||
|     # Check if hosted bluetooth is being used | ||||
|     if "esp32_hosted" in full_config: | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False) | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True) | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True) | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True) | ||||
|         add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) | ||||
|         add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) | ||||
|  | ||||
|     # Check if BLE Server is needed | ||||
|     has_ble_server = "esp32_ble_server" in full_config | ||||
|  | ||||
| @@ -374,6 +442,36 @@ def final_validation(config): | ||||
| FINAL_VALIDATE_SCHEMA = final_validation | ||||
|  | ||||
|  | ||||
| # This needs to be run as a job with CoroPriority.FINAL priority so that all components have | ||||
| # a chance to register their handlers before the counts are added to defines. | ||||
| @coroutine_with_priority(CoroPriority.FINAL) | ||||
| async def _add_ble_handler_defines(): | ||||
|     # Add defines for StaticVector sizing based on handler registration counts | ||||
|     # Only define if count > 0 to avoid allocating unnecessary memory | ||||
|     if _handler_counts.gap_event > 0: | ||||
|         cg.add_define( | ||||
|             "ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event | ||||
|         ) | ||||
|     if _handler_counts.gap_scan_event > 0: | ||||
|         cg.add_define( | ||||
|             "ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT", | ||||
|             _handler_counts.gap_scan_event, | ||||
|         ) | ||||
|     if _handler_counts.gattc_event > 0: | ||||
|         cg.add_define( | ||||
|             "ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT", _handler_counts.gattc_event | ||||
|         ) | ||||
|     if _handler_counts.gatts_event > 0: | ||||
|         cg.add_define( | ||||
|             "ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT", _handler_counts.gatts_event | ||||
|         ) | ||||
|     if _handler_counts.ble_status_event > 0: | ||||
|         cg.add_define( | ||||
|             "ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT", | ||||
|             _handler_counts.ble_status_event, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) | ||||
| @@ -396,8 +494,9 @@ async def to_code(config): | ||||
|     # Apply logger settings if log disabling is enabled | ||||
|     if config.get(CONF_DISABLE_BT_LOGS, False): | ||||
|         # Disable all Bluetooth loggers that are not required | ||||
|         required_loggers = _get_required_loggers() | ||||
|         for logger in BTLoggers: | ||||
|             if logger not in _required_loggers: | ||||
|             if logger not in required_loggers: | ||||
|                 add_idf_sdkconfig_option(f"{logger.value}_NONE", True) | ||||
|  | ||||
|     # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector | ||||
| @@ -428,6 +527,9 @@ async def to_code(config): | ||||
|         cg.add_define("USE_ESP32_BLE_ADVERTISING") | ||||
|         cg.add_define("USE_ESP32_BLE_UUID") | ||||
|  | ||||
|     # Schedule the handler defines to be added after all components register | ||||
|     CORE.add_job(_add_ble_handler_defines) | ||||
|  | ||||
|  | ||||
| @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) | ||||
| async def ble_enabled_to_code(config, condition_id, template_arg, args): | ||||
|   | ||||
| @@ -6,7 +6,15 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #include <esp_bt.h> | ||||
| #else | ||||
| extern "C" { | ||||
| #include <esp_hosted.h> | ||||
| #include <esp_hosted_misc.h> | ||||
| #include <esp_hosted_bluedroid.h> | ||||
| } | ||||
| #endif | ||||
| #include <esp_bt_device.h> | ||||
| #include <esp_bt_main.h> | ||||
| #include <esp_gap_ble_api.h> | ||||
| @@ -136,6 +144,7 @@ void ESP32BLE::advertising_init_() { | ||||
|  | ||||
| bool ESP32BLE::ble_setup_() { | ||||
|   esp_err_t err; | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #ifdef USE_ARDUINO | ||||
|   if (!btStart()) { | ||||
|     ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); | ||||
| @@ -169,6 +178,28 @@ bool ESP32BLE::ble_setup_() { | ||||
| #endif | ||||
|  | ||||
|   esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); | ||||
| #else | ||||
|   esp_hosted_connect_to_slave();  // NOLINT | ||||
|  | ||||
|   if (esp_hosted_bt_controller_init() != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (esp_hosted_bt_controller_enable() != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   hosted_hci_bluedroid_open(); | ||||
|  | ||||
|   esp_bluedroid_hci_driver_operations_t operations = { | ||||
|       .send = hosted_hci_bluedroid_send, | ||||
|       .check_send_available = hosted_hci_bluedroid_check_send_available, | ||||
|       .register_host_callback = hosted_hci_bluedroid_register_host_callback, | ||||
|   }; | ||||
|   esp_bluedroid_attach_hci_driver(&operations); | ||||
| #endif | ||||
|  | ||||
|   err = esp_bluedroid_init(); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -181,31 +212,27 @@ bool ESP32BLE::ble_setup_() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (!this->gap_event_handlers_.empty()) { | ||||
|     err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_SERVER | ||||
|   if (!this->gatts_event_handlers_.empty()) { | ||||
|     err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); | ||||
|       return false; | ||||
|     } | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|   err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); | ||||
|     return false; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32_BLE_CLIENT | ||||
|   if (!this->gattc_event_handlers_.empty()) { | ||||
|     err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); | ||||
|       return false; | ||||
|     } | ||||
| #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) | ||||
|   err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); | ||||
|     return false; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) | ||||
|   err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); | ||||
|     return false; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| @@ -213,8 +240,11 @@ bool ESP32BLE::ble_setup_() { | ||||
|   if (this->name_.has_value()) { | ||||
|     name = this->name_.value(); | ||||
|     if (App.is_name_add_mac_suffix_enabled()) { | ||||
|       name += "-"; | ||||
|       name += get_mac_address().substr(6); | ||||
|       // MAC address suffix length (last 6 characters of 12-char MAC address string) | ||||
|       constexpr size_t mac_address_suffix_len = 6; | ||||
|       const std::string mac_addr = get_mac_address(); | ||||
|       const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; | ||||
|       name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); | ||||
|     } | ||||
|   } else { | ||||
|     name = App.get_name(); | ||||
| @@ -258,6 +288,7 @@ bool ESP32BLE::ble_dismantle_() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #ifdef USE_ARDUINO | ||||
|   if (!btStop()) { | ||||
|     ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status()); | ||||
| @@ -287,6 +318,19 @@ bool ESP32BLE::ble_dismantle_() { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
| #else | ||||
|   if (esp_hosted_bt_controller_disable() != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (esp_hosted_bt_controller_deinit(false) != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   hosted_hci_bluedroid_close(); | ||||
| #endif | ||||
|   return true; | ||||
| } | ||||
| @@ -299,9 +343,11 @@ void ESP32BLE::loop() { | ||||
|     case BLE_COMPONENT_STATE_DISABLE: { | ||||
|       ESP_LOGD(TAG, "Disabling"); | ||||
|  | ||||
| #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT | ||||
|       for (auto *ble_event_handler : this->ble_status_event_handlers_) { | ||||
|         ble_event_handler->ble_before_disabled_event_handler(); | ||||
|       } | ||||
| #endif | ||||
|  | ||||
|       if (!ble_dismantle_()) { | ||||
|         ESP_LOGE(TAG, "Could not be dismantled"); | ||||
| @@ -331,7 +377,7 @@ void ESP32BLE::loop() { | ||||
|   BLEEvent *ble_event = this->ble_events_.pop(); | ||||
|   while (ble_event != nullptr) { | ||||
|     switch (ble_event->type_) { | ||||
| #ifdef USE_ESP32_BLE_SERVER | ||||
| #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) | ||||
|       case BLEEvent::GATTS: { | ||||
|         esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; | ||||
|         esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; | ||||
| @@ -343,7 +389,7 @@ void ESP32BLE::loop() { | ||||
|         break; | ||||
|       } | ||||
| #endif | ||||
| #ifdef USE_ESP32_BLE_CLIENT | ||||
| #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) | ||||
|       case BLEEvent::GATTC: { | ||||
|         esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; | ||||
|         esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; | ||||
| @@ -359,10 +405,12 @@ void ESP32BLE::loop() { | ||||
|         esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; | ||||
|         switch (gap_event) { | ||||
|           case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT | ||||
|             // Use the new scan event handler - no memcpy! | ||||
|             for (auto *scan_handler : this->gap_scan_event_handlers_) { | ||||
|               scan_handler->gap_scan_event_handler(ble_event->scan_result()); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           // Scan complete events | ||||
| @@ -374,10 +422,12 @@ void ESP32BLE::loop() { | ||||
|             // This is verified at compile-time by static_assert checks in ble_event.h | ||||
|             // The struct already contains our copy of the status (copied in BLEEvent constructor) | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           // Advertising complete events | ||||
| @@ -388,19 +438,23 @@ void ESP32BLE::loop() { | ||||
|           case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: | ||||
|             // All advertising complete events have the same structure with just status | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           // RSSI complete event | ||||
|           case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           // Security events | ||||
| @@ -410,10 +464,12 @@ void ESP32BLE::loop() { | ||||
|           case ESP_GAP_BLE_PASSKEY_REQ_EVT: | ||||
|           case ESP_GAP_BLE_NC_REQ_EVT: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           default: | ||||
|   | ||||
| @@ -125,19 +125,25 @@ class ESP32BLE : public Component { | ||||
|   void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); | ||||
| #endif | ||||
|  | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|   void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } | ||||
| #endif | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT | ||||
|   void register_gap_scan_event_handler(GAPScanEventHandler *handler) { | ||||
|     this->gap_scan_event_handlers_.push_back(handler); | ||||
|   } | ||||
| #ifdef USE_ESP32_BLE_CLIENT | ||||
| #endif | ||||
| #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) | ||||
|   void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } | ||||
| #endif | ||||
| #ifdef USE_ESP32_BLE_SERVER | ||||
| #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) | ||||
|   void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } | ||||
| #endif | ||||
| #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT | ||||
|   void register_ble_status_event_handler(BLEStatusEventHandler *handler) { | ||||
|     this->ble_status_event_handlers_.push_back(handler); | ||||
|   } | ||||
| #endif | ||||
|   void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } | ||||
|  | ||||
|  protected: | ||||
| @@ -159,16 +165,22 @@ class ESP32BLE : public Component { | ||||
|  private: | ||||
|   template<typename... Args> friend void enqueue_ble_event(Args... args); | ||||
|  | ||||
|   // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes) | ||||
|   std::vector<GAPEventHandler *> gap_event_handlers_; | ||||
|   std::vector<GAPScanEventHandler *> gap_scan_event_handlers_; | ||||
| #ifdef USE_ESP32_BLE_CLIENT | ||||
|   std::vector<GATTcEventHandler *> gattc_event_handlers_; | ||||
|   // Handler vectors - use StaticVector when counts are known at compile time | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|   StaticVector<GAPEventHandler *, ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT> gap_event_handlers_; | ||||
| #endif | ||||
| #ifdef USE_ESP32_BLE_SERVER | ||||
|   std::vector<GATTsEventHandler *> gatts_event_handlers_; | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT | ||||
|   StaticVector<GAPScanEventHandler *, ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT> gap_scan_event_handlers_; | ||||
| #endif | ||||
| #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) | ||||
|   StaticVector<GATTcEventHandler *, ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT> gattc_event_handlers_; | ||||
| #endif | ||||
| #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) | ||||
|   StaticVector<GATTsEventHandler *, ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT> gatts_event_handlers_; | ||||
| #endif | ||||
| #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT | ||||
|   StaticVector<BLEStatusEventHandler *, ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT> ble_status_event_handlers_; | ||||
| #endif | ||||
|   std::vector<BLEStatusEventHandler *> ble_status_event_handlers_; | ||||
|  | ||||
|   // Large objects (size depends on template parameters, but typically aligned to 4 bytes) | ||||
|   esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; | ||||
|   | ||||
| @@ -10,7 +10,9 @@ | ||||
| #ifdef USE_ESP32 | ||||
| #ifdef USE_ESP32_BLE_ADVERTISING | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #include <esp_bt.h> | ||||
| #endif | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gatts_api.h> | ||||
|  | ||||
|   | ||||
| @@ -74,7 +74,7 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID], uuid_arr) | ||||
|  | ||||
|     parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) | ||||
|     cg.add(parent.register_gap_event_handler(var)) | ||||
|     esp32_ble.register_gap_event_handler(parent, var) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|     cg.add(var.set_major(config[CONF_MAJOR])) | ||||
|   | ||||
| @@ -3,7 +3,9 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #include <esp_bt.h> | ||||
| #endif | ||||
| #include <esp_bt_main.h> | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
|   | ||||
| @@ -5,7 +5,9 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID | ||||
| #include <esp_bt.h> | ||||
| #endif | ||||
| #include <esp_gap_ble_api.h> | ||||
|  | ||||
| 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