mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:18:45 +00:00 
			
		
		
		
	Compare commits
	
		
			85 Commits
		
	
	
		
			fix_clang_
			...
			template_l
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | be71aa4f1d | ||
|   | 581732e0ca | ||
|   | 61e2b2b6eb | ||
|   | e202b933d9 | ||
|   | 5344318c0d | ||
|   | ef6a7ed79e | ||
|   | 2faca19947 | ||
|   | a5fc52ccbb | ||
|   | 66fd919e07 | ||
|   | 50b23aa988 | ||
|   | afd0c85e48 | ||
|   | 7394cbf773 | ||
|   | 1577a46efd | ||
|   | e212ed024d | ||
|   | 5fdd90c71a | ||
|   | 6929bdb415 | ||
|   | 2c85ba037e | ||
|   | 2440bbdceb | ||
|   | 3ac8eb7696 | ||
|   | 6a478b9070 | ||
|   | a32a1d11fb | ||
|   | daeb8ef88c | ||
|   | febee437d6 | ||
|   | de2f475dbd | ||
|   | fa3ec6f732 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e490aec6b4 | ||
|   | 8da8095a6a | ||
|   | ab14c0cd72 | ||
|   | 917deac7cb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3d21adecd3 | ||
|   | 5b023f9369 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 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 | 
| @@ -1 +1 @@ | ||||
| d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248 | ||||
| 3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.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; | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -178,6 +178,8 @@ jobs: | ||||
|       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 }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
| @@ -210,6 +212,8 @@ jobs: | ||||
|           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 | ||||
|  | ||||
|   integration-tests: | ||||
|     name: Run integration tests | ||||
| @@ -247,6 +251,33 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||
|  | ||||
|   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 | ||||
|   | ||||
							
								
								
									
										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 | ||||
|   | ||||
| @@ -201,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 | ||||
|   | ||||
| @@ -231,9 +231,22 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): | ||||
|                 api_component = (name, mem) | ||||
|                 break | ||||
|  | ||||
|         # Combine all components to analyze: top ESPHome + all external + API if not already included | ||||
|         components_to_analyze = list(top_esphome_components) + list( | ||||
|             top_external_components | ||||
|         # 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) | ||||
|   | ||||
| @@ -127,40 +127,39 @@ SYMBOL_PATTERNS = { | ||||
|         "tryget_socket_unconn", | ||||
|         "cs_create_ctrl_sock", | ||||
|         "netbuf_alloc", | ||||
|         "tcp_",  # TCP protocol functions | ||||
|         "udp_",  # UDP protocol functions | ||||
|         "lwip_",  # LwIP stack functions | ||||
|         "eagle_lwip",  # ESP-specific LwIP functions | ||||
|         "new_linkoutput",  # Link output function | ||||
|         "acd_",  # Address Conflict Detection (ACD) | ||||
|         "eth_",  # Ethernet functions | ||||
|         "mac_enable_bb",  # MAC baseband enable | ||||
|         "reassemble_and_dispatch",  # Packet reassembly | ||||
|     ], | ||||
|     # dhcp must come before libc to avoid "dhcp_select" matching "select" pattern | ||||
|     "dhcp": ["dhcp", "handle_dhcp"], | ||||
|     "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], | ||||
|     "wifi_stack": [ | ||||
|         "ieee80211", | ||||
|         "hostap", | ||||
|         "sta_", | ||||
|         "ap_", | ||||
|         "scan_", | ||||
|         "wifi_", | ||||
|         "wpa_", | ||||
|         "wps_", | ||||
|         "esp_wifi", | ||||
|         "cnx_", | ||||
|         "wpa3_", | ||||
|         "sae_", | ||||
|         "wDev_", | ||||
|         "ic_", | ||||
|         "mac_", | ||||
|         "esf_buf", | ||||
|         "gWpaSm", | ||||
|         "sm_WPA", | ||||
|         "eapol_", | ||||
|         "owe_", | ||||
|         "wifiLowLevelInit", | ||||
|         "s_do_mapping", | ||||
|         "gScanStruct", | ||||
|         "ppSearchTxframe", | ||||
|         "ppMapWaitTxq", | ||||
|         "ppFillAMPDUBar", | ||||
|         "ppCheckTxConnTrafficIdle", | ||||
|         "ppCalTkipMic", | ||||
|     # Order matters! More specific categories must come before general ones. | ||||
|     # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern | ||||
|     "mdns_lib": ["mdns"], | ||||
|     # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols | ||||
|     "memory_mgmt": [ | ||||
|         "mem_", | ||||
|         "memory_", | ||||
|         "tlsf_", | ||||
|         "memp_", | ||||
|         "pbuf_", | ||||
|         "pbuf_alloc", | ||||
|         "pbuf_copy_partial_pbuf", | ||||
|         "esp_mmu_map", | ||||
|         "mmu_hal_", | ||||
|         "s_do_mapping",  # Memory mapping function, not WiFi | ||||
|         "hash_map_",  # Hash map data structure | ||||
|         "umm_assimilate",  # UMM malloc assimilation | ||||
|     ], | ||||
|     "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], | ||||
|     "wifi_bt_coex": ["coex"], | ||||
|     # Bluetooth categories must come BEFORE wifi_stack to avoid misclassification | ||||
|     # Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns | ||||
|     "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], | ||||
|     "bluedroid_bt": [ | ||||
|         "bluedroid", | ||||
| @@ -207,6 +206,61 @@ SYMBOL_PATTERNS = { | ||||
|         "copy_extra_byte_in_db", | ||||
|         "parse_read_local_supported_commands_response", | ||||
|     ], | ||||
|     "bluetooth": [ | ||||
|         "bt_", | ||||
|         "_ble_",  # More specific than "ble_" to avoid matching "able_", "enable_", "disable_" | ||||
|         "l2c_", | ||||
|         "l2ble_",  # L2CAP for BLE | ||||
|         "gatt_", | ||||
|         "gap_", | ||||
|         "hci_", | ||||
|         "btsnd_hcic_",  # Bluetooth HCI command send functions | ||||
|         "BT_init", | ||||
|         "BT_tx_",  # Bluetooth transmit functions | ||||
|         "esp_ble_",  # Catch esp_ble_* functions | ||||
|     ], | ||||
|     "bluetooth_ll": [ | ||||
|         "llm_",  # Link layer manager | ||||
|         "llc_",  # Link layer control | ||||
|         "lld_",  # Link layer driver | ||||
|         "ld_acl_",  # Link layer ACL (Asynchronous Connection-Oriented) | ||||
|         "llcp_",  # Link layer control protocol | ||||
|         "lmp_",  # Link manager protocol | ||||
|     ], | ||||
|     "wifi_bt_coex": ["coex"], | ||||
|     "wifi_stack": [ | ||||
|         "ieee80211", | ||||
|         "hostap", | ||||
|         "sta_", | ||||
|         "wifi_ap_",  # More specific than "ap_" to avoid matching "cap_", "map_" | ||||
|         "wifi_scan_",  # More specific than "scan_" to avoid matching "_scan_" in other contexts | ||||
|         "wifi_", | ||||
|         "wpa_", | ||||
|         "wps_", | ||||
|         "esp_wifi", | ||||
|         "cnx_", | ||||
|         "wpa3_", | ||||
|         "sae_", | ||||
|         "wDev_", | ||||
|         "ic_mac_",  # More specific than "mac_" to avoid matching emac_ | ||||
|         "esf_buf", | ||||
|         "gWpaSm", | ||||
|         "sm_WPA", | ||||
|         "eapol_", | ||||
|         "owe_", | ||||
|         "wifiLowLevelInit", | ||||
|         # Removed "s_do_mapping" - this is memory management, not WiFi | ||||
|         "gScanStruct", | ||||
|         "ppSearchTxframe", | ||||
|         "ppMapWaitTxq", | ||||
|         "ppFillAMPDUBar", | ||||
|         "ppCheckTxConnTrafficIdle", | ||||
|         "ppCalTkipMic", | ||||
|         "phy_force_wifi", | ||||
|         "phy_unforce_wifi", | ||||
|         "write_wifi_chan", | ||||
|         "wifi_track_pll", | ||||
|     ], | ||||
|     "crypto_math": [ | ||||
|         "ecp_", | ||||
|         "bignum_", | ||||
| @@ -231,13 +285,36 @@ SYMBOL_PATTERNS = { | ||||
|         "p_256_init_curve", | ||||
|         "shift_sub_rows", | ||||
|         "rshift", | ||||
|         "rijndaelEncrypt",  # AES Rijndael encryption | ||||
|     ], | ||||
|     # System and Arduino core functions must come before libc | ||||
|     "esp_system": [ | ||||
|         "system_",  # ESP system functions | ||||
|         "postmortem_",  # Postmortem reporting | ||||
|     ], | ||||
|     "arduino_core": [ | ||||
|         "pinMode", | ||||
|         "resetPins", | ||||
|         "millis", | ||||
|         "micros", | ||||
|         "delay(",  # More specific - Arduino delay function with parenthesis | ||||
|         "delayMicroseconds", | ||||
|         "digitalWrite", | ||||
|         "digitalRead", | ||||
|     ], | ||||
|     "sntp": ["sntp_", "sntp_recv"], | ||||
|     "scheduler": [ | ||||
|         "run_scheduled_", | ||||
|         "compute_scheduled_", | ||||
|         "event_TaskQueue", | ||||
|     ], | ||||
|     "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], | ||||
|     "libc": [ | ||||
|         "printf", | ||||
|         "scanf", | ||||
|         "malloc", | ||||
|         "free", | ||||
|         "_free",  # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring | ||||
|         "umm_free",  # UMM malloc free function | ||||
|         "memcpy", | ||||
|         "memset", | ||||
|         "strcpy", | ||||
| @@ -259,7 +336,7 @@ SYMBOL_PATTERNS = { | ||||
|         "_setenv_r", | ||||
|         "_tzset_unlocked_r", | ||||
|         "__tzcalc_limits", | ||||
|         "select", | ||||
|         "_select",  # More specific than "select" to avoid matching "dhcp_select", etc. | ||||
|         "scalbnf", | ||||
|         "strtof", | ||||
|         "strtof_l", | ||||
| @@ -316,8 +393,24 @@ SYMBOL_PATTERNS = { | ||||
|         "CSWTCH$", | ||||
|         "dst$", | ||||
|         "sulp", | ||||
|         "_strtol_l",  # String to long with locale | ||||
|         "__cvt",  # Convert | ||||
|         "__utoa",  # Unsigned to ASCII | ||||
|         "__global_locale",  # Global locale | ||||
|         "_ctype_",  # Character type | ||||
|         "impure_data",  # Impure data | ||||
|     ], | ||||
|     "string_ops": [ | ||||
|         "strcmp", | ||||
|         "strncmp", | ||||
|         "strchr", | ||||
|         "strstr", | ||||
|         "strtok", | ||||
|         "strdup", | ||||
|         "strncasecmp_P",  # String compare (case insensitive, from program memory) | ||||
|         "strnlen_P",  # String length (from program memory) | ||||
|         "strncat_P",  # String concatenate (from program memory) | ||||
|     ], | ||||
|     "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], | ||||
|     "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], | ||||
|     "file_io": [ | ||||
|         "fread", | ||||
| @@ -338,10 +431,26 @@ SYMBOL_PATTERNS = { | ||||
|         "vsscanf", | ||||
|     ], | ||||
|     "cpp_anonymous": ["_GLOBAL__N_", "n$"], | ||||
|     "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], | ||||
|     "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], | ||||
|     # Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS | ||||
|     "nvs": ["nvs_"],  # Plain C NVS functions | ||||
|     "ota": ["ota_", "OTA", "esp_ota", "app_desc"], | ||||
|     # cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly | ||||
|     # Only keep patterns that are truly runtime-specific and not categorizable by namespace | ||||
|     "cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"], | ||||
|     "exception_handling": [ | ||||
|         "__cxa_", | ||||
|         "_Unwind_", | ||||
|         "__gcc_personality", | ||||
|         "uw_frame_state", | ||||
|         "search_object",  # Search for exception handling object | ||||
|         "get_cie_encoding",  # Get CIE encoding | ||||
|         "add_fdes",  # Add frame description entries | ||||
|         "fde_unencoded_compare",  # Compare FDEs | ||||
|         "fde_mixed_encoding_compare",  # Compare mixed encoding FDEs | ||||
|         "frame_downheap",  # Frame heap operations | ||||
|         "frame_heapsort",  # Frame heap sorting | ||||
|     ], | ||||
|     "static_init": ["_GLOBAL__sub_I_"], | ||||
|     "mdns_lib": ["mdns"], | ||||
|     "phy_radio": [ | ||||
|         "phy_", | ||||
|         "rf_", | ||||
| @@ -394,10 +503,47 @@ SYMBOL_PATTERNS = { | ||||
|         "txcal_debuge_mode", | ||||
|         "ant_wifitx_cfg", | ||||
|         "reg_init_begin", | ||||
|         "tx_cap_init",  # TX capacitance init | ||||
|         "ram_set_txcap",  # RAM TX capacitance setting | ||||
|         "tx_atten_",  # TX attenuation | ||||
|         "txiq_",  # TX I/Q calibration | ||||
|         "ram_cal_",  # RAM calibration | ||||
|         "ram_rxiq_",  # RAM RX I/Q | ||||
|         "readvdd33",  # Read VDD33 | ||||
|         "test_tout",  # Test timeout | ||||
|         "tsen_meas",  # Temperature sensor measurement | ||||
|         "bbpll_cal",  # Baseband PLL calibration | ||||
|         "set_cal_",  # Set calibration | ||||
|         "set_rfanagain_",  # Set RF analog gain | ||||
|         "set_txdc_",  # Set TX DC | ||||
|         "get_vdd33_",  # Get VDD33 | ||||
|         "gen_rx_gain_table",  # Generate RX gain table | ||||
|         "ram_ana_inf_gating_en",  # RAM analog interface gating enable | ||||
|         "tx_cont_en",  # TX continuous enable | ||||
|         "tx_delay_cfg",  # TX delay configuration | ||||
|         "tx_gain_table_set",  # TX gain table set | ||||
|         "check_and_reset_hw_deadlock",  # Hardware deadlock check | ||||
|         "s_config",  # System/hardware config | ||||
|         "chan14_mic_cfg",  # Channel 14 MIC config | ||||
|     ], | ||||
|     "wifi_phy_pp": [ | ||||
|         "pp_", | ||||
|         "ppT", | ||||
|         "ppR", | ||||
|         "ppP", | ||||
|         "ppInstall", | ||||
|         "ppCalTxAMPDULength", | ||||
|         "ppCheckTx",  # Packet processor TX check | ||||
|         "ppCal",  # Packet processor calibration | ||||
|         "HdlAllBuffedEb",  # Handle buffered EB | ||||
|     ], | ||||
|     "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], | ||||
|     "wifi_lmac": ["lmac"], | ||||
|     "wifi_device": ["wdev", "wDev_"], | ||||
|     "wifi_device": [ | ||||
|         "wdev", | ||||
|         "wDev_", | ||||
|         "ic_set_sta",  # Set station mode | ||||
|         "ic_set_vif",  # Set virtual interface | ||||
|     ], | ||||
|     "power_mgmt": [ | ||||
|         "pm_", | ||||
|         "sleep", | ||||
| @@ -406,15 +552,7 @@ SYMBOL_PATTERNS = { | ||||
|         "deep_sleep", | ||||
|         "power_down", | ||||
|         "g_pm", | ||||
|     ], | ||||
|     "memory_mgmt": [ | ||||
|         "mem_", | ||||
|         "memory_", | ||||
|         "tlsf_", | ||||
|         "memp_", | ||||
|         "pbuf_", | ||||
|         "pbuf_alloc", | ||||
|         "pbuf_copy_partial_pbuf", | ||||
|         "pmc",  # Power Management Controller | ||||
|     ], | ||||
|     "hal_layer": ["hal_"], | ||||
|     "clock_mgmt": [ | ||||
| @@ -439,7 +577,6 @@ SYMBOL_PATTERNS = { | ||||
|     "error_handling": ["panic", "abort", "assert", "error_", "fault"], | ||||
|     "authentication": ["auth"], | ||||
|     "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], | ||||
|     "dhcp": ["dhcp", "handle_dhcp"], | ||||
|     "ethernet_phy": [ | ||||
|         "emac_", | ||||
|         "eth_phy_", | ||||
| @@ -618,7 +755,15 @@ SYMBOL_PATTERNS = { | ||||
|         "ampdu_dispatch_upto", | ||||
|     ], | ||||
|     "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], | ||||
|     "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], | ||||
|     "rate_control": [ | ||||
|         "rssi_margin", | ||||
|         "rcGetSched", | ||||
|         "get_rate_fcc_index", | ||||
|         "rcGetRate",  # Get rate | ||||
|         "rc_get_",  # Rate control getters | ||||
|         "rc_set_",  # Rate control setters | ||||
|         "rc_enable_",  # Rate control enable functions | ||||
|     ], | ||||
|     "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], | ||||
|     "channel_mgmt": ["chm_init", "chm_set_current_channel"], | ||||
|     "trace": ["trc_init", "trc_onAmpduOp"], | ||||
| @@ -799,31 +944,18 @@ SYMBOL_PATTERNS = { | ||||
|         "supports_interlaced_inquiry_scan", | ||||
|         "supports_reading_remote_extended_features", | ||||
|     ], | ||||
|     "bluetooth_ll": [ | ||||
|         "lld_pdu_", | ||||
|         "ld_acl_", | ||||
|         "lld_stop_ind_handler", | ||||
|         "lld_evt_winsize_change", | ||||
|         "config_lld_evt_funcs_reset", | ||||
|         "config_lld_funcs_reset", | ||||
|         "config_llm_funcs_reset", | ||||
|         "llm_set_long_adv_data", | ||||
|         "lld_retry_tx_prog", | ||||
|         "llc_link_sup_to_ind_handler", | ||||
|         "config_llc_funcs_reset", | ||||
|         "lld_evt_rxwin_compute", | ||||
|         "config_btdm_funcs_reset", | ||||
|         "config_ea_funcs_reset", | ||||
|         "llc_defalut_state_tab_reset", | ||||
|         "config_rwip_funcs_reset", | ||||
|         "ke_lmp_rx_flooding_detect", | ||||
|     ], | ||||
| } | ||||
|  | ||||
| # Demangled patterns: patterns found in demangled C++ names | ||||
| DEMANGLED_PATTERNS = { | ||||
|     "gpio_driver": ["GPIO"], | ||||
|     "uart_driver": ["UART"], | ||||
|     # mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder | ||||
|     "mdns_lib": [ | ||||
|         "MDNSResponder", | ||||
|         "MDNSImplementation", | ||||
|         "MDNS", | ||||
|     ], | ||||
|     "network_stack": [ | ||||
|         "lwip", | ||||
|         "tcp", | ||||
| @@ -836,6 +968,24 @@ DEMANGLED_PATTERNS = { | ||||
|         "ethernet", | ||||
|         "ppp", | ||||
|         "slip", | ||||
|         "UdpContext",  # UDP context class | ||||
|         "DhcpServer",  # DHCP server class | ||||
|     ], | ||||
|     "arduino_core": [ | ||||
|         "String::",  # Arduino String class | ||||
|         "Print::",  # Arduino Print class | ||||
|         "HardwareSerial::",  # Serial class | ||||
|         "IPAddress::",  # IP address class | ||||
|         "EspClass::",  # ESP class | ||||
|         "experimental::_SPI",  # Experimental SPI | ||||
|     ], | ||||
|     "ota": [ | ||||
|         "UpdaterClass", | ||||
|         "Updater::", | ||||
|     ], | ||||
|     "wifi": [ | ||||
|         "ESP8266WiFi", | ||||
|         "WiFi::", | ||||
|     ], | ||||
|     "wifi_stack": ["NetworkInterface"], | ||||
|     "nimble_bt": [ | ||||
| @@ -854,7 +1004,6 @@ DEMANGLED_PATTERNS = { | ||||
|     "rtti": ["__type_info", "__class_type_info"], | ||||
|     "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], | ||||
|     "async_tcp": ["AsyncClient", "AsyncServer"], | ||||
|     "mdns_lib": ["mdns"], | ||||
|     "json_lib": [ | ||||
|         "ArduinoJson", | ||||
|         "JsonDocument", | ||||
|   | ||||
| @@ -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 | ||||
| @@ -100,6 +105,36 @@ LambdaCondition = cg.esphome_ns.class_("LambdaCondition", 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 = {} | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -486,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, | ||||
| @@ -1572,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"); | ||||
|   | ||||
| @@ -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 | ||||
| @@ -174,6 +175,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 | ||||
|   | ||||
| @@ -264,20 +264,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 | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -385,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::set has consistent order (lexicographic for strings) | ||||
|     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()) { | ||||
| @@ -400,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::set has consistent order (lexicographic for strings) | ||||
|     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()) { | ||||
| @@ -520,13 +524,23 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   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; | ||||
| @@ -545,29 +559,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   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(); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
|     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); | ||||
|  | ||||
| @@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { | ||||
|     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); | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -304,9 +304,13 @@ 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: | ||||
| @@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { | ||||
| 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"), | ||||
| @@ -550,6 +555,32 @@ 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: | ||||
| @@ -615,6 +646,13 @@ FRAMEWORK_SCHEMA = cv.All( | ||||
|                     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, | ||||
|                 } | ||||
|             ), | ||||
| @@ -844,6 +882,11 @@ 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", | ||||
| @@ -962,6 +1005,43 @@ async def to_code(config): | ||||
|     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( | ||||
|   | ||||
| @@ -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]))) | ||||
|   | ||||
| @@ -112,7 +112,7 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_define("USE_IMPROV") | ||||
|  | ||||
|     await improv_base.setup_improv_core(var, config) | ||||
|     await improv_base.setup_improv_core(var, config, "esp32_improv") | ||||
|  | ||||
|     cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) | ||||
|     cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) | ||||
|   | ||||
| @@ -389,11 +389,13 @@ void ESP32ImprovComponent::check_wifi_connection_() { | ||||
|     std::string url_strings[3]; | ||||
|     size_t url_count = 0; | ||||
|  | ||||
| #ifdef USE_ESP32_IMPROV_NEXT_URL | ||||
|     // Add next_url if configured (should be first per Improv BLE spec) | ||||
|     std::string next_url = this->get_formatted_next_url_(); | ||||
|     if (!next_url.empty()) { | ||||
|       url_strings[url_count++] = std::move(next_url); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     // Add default URLs for backward compatibility | ||||
|     url_strings[url_count++] = ESPHOME_MY_LINK; | ||||
|   | ||||
| @@ -190,7 +190,9 @@ async def to_code(config): | ||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||
|     cg.add_define(ThreadModel.SINGLE) | ||||
|  | ||||
|     cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"]) | ||||
|     cg.add_platformio_option( | ||||
|         "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] | ||||
|     ) | ||||
|  | ||||
|     conf = config[CONF_FRAMEWORK] | ||||
|     cg.add_platformio_option("framework", "arduino") | ||||
| @@ -230,9 +232,9 @@ async def to_code(config): | ||||
|     # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` | ||||
|     cg.add_build_flag("-DNEW_OOM_ABORT") | ||||
|  | ||||
|     # In testing mode, fake a larger IRAM to allow linking grouped component tests | ||||
|     # Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB | ||||
|     # This is done via a pre-build script that generates a custom linker script | ||||
|     # In testing mode, fake larger memory to allow linking grouped component tests | ||||
|     # Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing | ||||
|     # we pretend it has much larger memory to test that components compile together | ||||
|     if CORE.testing_mode: | ||||
|         cg.add_build_flag("-DESPHOME_TESTING_MODE") | ||||
|  | ||||
| @@ -271,8 +273,8 @@ def copy_files(): | ||||
|         post_build_file, | ||||
|         CORE.relative_build_path("post_build.py"), | ||||
|     ) | ||||
|     iram_fix_file = dir / "iram_fix.py.script" | ||||
|     testing_mode_file = dir / "testing_mode.py.script" | ||||
|     copy_file_if_changed( | ||||
|         iram_fix_file, | ||||
|         CORE.relative_build_path("iram_fix.py"), | ||||
|         testing_mode_file, | ||||
|         CORE.relative_build_path("testing_mode.py"), | ||||
|     ) | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp8266 | ||||
|   | ||||
| @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): | ||||
|     num = config[CONF_NUMBER] | ||||
|     mode = config[CONF_MODE] | ||||
|     cg.add(var.set_pin(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)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(mode))) | ||||
|     if num < 16: | ||||
|         initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ | ||||
|   | ||||
| @@ -1,44 +0,0 @@ | ||||
| import os | ||||
| import re | ||||
|  | ||||
| # pylint: disable=E0602 | ||||
| Import("env")  # noqa | ||||
|  | ||||
|  | ||||
| def patch_linker_script_after_preprocess(source, target, env): | ||||
|     """Patch the local linker script after PlatformIO preprocesses it.""" | ||||
|     # 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 | ||||
|  | ||||
|     # Get the local linker script path | ||||
|     build_dir = env.subst("$BUILD_DIR") | ||||
|     local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld") | ||||
|  | ||||
|     if not os.path.exists(local_ld): | ||||
|         return | ||||
|  | ||||
|     # Read the linker script | ||||
|     with open(local_ld, "r") as f: | ||||
|         content = f.read() | ||||
|  | ||||
|     # Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB) | ||||
|     # The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000 | ||||
|     updated = re.sub( | ||||
|         r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000", | ||||
|         r"\g<1>0x200000", | ||||
|         content, | ||||
|     ) | ||||
|  | ||||
|     if updated != content: | ||||
|         with open(local_ld, "w") as f: | ||||
|             f.write(updated) | ||||
|         print("ESPHome: Patched IRAM size to 2MB for testing mode") | ||||
|  | ||||
|  | ||||
| # Hook into the build process right before linking | ||||
| # This runs after PlatformIO has already preprocessed the linker scripts | ||||
| env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess) | ||||
							
								
								
									
										166
									
								
								esphome/components/esp8266/testing_mode.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								esphome/components/esp8266/testing_mode.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| import os | ||||
| import re | ||||
|  | ||||
| # pylint: disable=E0602 | ||||
| Import("env")  # noqa | ||||
|  | ||||
|  | ||||
| # Memory sizes for testing mode (allow larger builds for CI component grouping) | ||||
| TESTING_IRAM_SIZE = "0x200000"  # 2MB | ||||
| TESTING_DRAM_SIZE = "0x200000"  # 2MB | ||||
| TESTING_FLASH_SIZE = "0x2000000"  # 32MB | ||||
|  | ||||
|  | ||||
| def patch_segment_size(content, segment_name, new_size, label): | ||||
|     """Patch a memory segment's length in linker script. | ||||
|  | ||||
|     Args: | ||||
|         content: Linker script content | ||||
|         segment_name: Name of the segment (e.g., 'iram1_0_seg') | ||||
|         new_size: New size as hex string (e.g., '0x200000') | ||||
|         label: Human-readable label for logging (e.g., 'IRAM') | ||||
|  | ||||
|     Returns: | ||||
|         Tuple of (patched_content, was_patched) | ||||
|     """ | ||||
|     # Match: segment_name : org = 0x..., len = 0x... | ||||
|     pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+" | ||||
|     new_content = re.sub(pattern, rf"\g<1>{new_size}", content) | ||||
|     return new_content, new_content != content | ||||
|  | ||||
|  | ||||
| def apply_memory_patches(content): | ||||
|     """Apply IRAM, DRAM, and Flash patches to linker script content. | ||||
|  | ||||
|     Args: | ||||
|         content: Linker script content as string | ||||
|  | ||||
|     Returns: | ||||
|         Patched content as string | ||||
|     """ | ||||
|     patches_applied = [] | ||||
|  | ||||
|     # Patch IRAM (for larger code in IRAM) | ||||
|     content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM") | ||||
|     if patched: | ||||
|         patches_applied.append("IRAM") | ||||
|  | ||||
|     # Patch DRAM (for larger BSS/data sections) | ||||
|     content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM") | ||||
|     if patched: | ||||
|         patches_applied.append("DRAM") | ||||
|  | ||||
|     # Patch Flash (for larger code sections) | ||||
|     content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash") | ||||
|     if patched: | ||||
|         patches_applied.append("Flash") | ||||
|  | ||||
|     if patches_applied: | ||||
|         iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024) | ||||
|         dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024) | ||||
|         flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024) | ||||
|         print(f"  Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)") | ||||
|  | ||||
|     return content | ||||
|  | ||||
|  | ||||
| def patch_linker_script_file(filepath, description): | ||||
|     """Patch a linker script file in the build directory with enlarged memory segments. | ||||
|  | ||||
|     This function modifies linker scripts in the build directory only (never SDK files). | ||||
|     It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode. | ||||
|  | ||||
|     Args: | ||||
|         filepath: Path to the linker script file in the build directory | ||||
|         description: Human-readable description for logging | ||||
|  | ||||
|     Returns: | ||||
|         True if the file was patched, False if already patched or not found | ||||
|     """ | ||||
|     if not os.path.exists(filepath): | ||||
|         print(f"ESPHome: {description} not found at {filepath}") | ||||
|         return False | ||||
|  | ||||
|     print(f"ESPHome: Patching {description}...") | ||||
|     with open(filepath, "r") as f: | ||||
|         content = f.read() | ||||
|  | ||||
|     patched_content = apply_memory_patches(content) | ||||
|  | ||||
|     if patched_content != content: | ||||
|         with open(filepath, "w") as f: | ||||
|             f.write(patched_content) | ||||
|         print(f"ESPHome: Successfully patched {description}") | ||||
|         return True | ||||
|     else: | ||||
|         print(f"ESPHome: {description} already patched or no changes needed") | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def patch_local_linker_script(source, target, env): | ||||
|     """Patch the local.eagle.app.v6.common.ld in build directory. | ||||
|  | ||||
|     This patches the preprocessed linker script that PlatformIO creates in the build | ||||
|     directory, enlarging IRAM, DRAM, and Flash segments for testing mode. | ||||
|  | ||||
|     Args: | ||||
|         source: SCons source nodes | ||||
|         target: SCons target nodes | ||||
|         env: SCons environment | ||||
|     """ | ||||
|     # Check if we're in testing mode | ||||
|     build_flags = env.get("BUILD_FLAGS", []) | ||||
|     testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) | ||||
|  | ||||
|     if not testing_mode: | ||||
|         return | ||||
|  | ||||
|     # Patch the local linker script if it exists | ||||
|     build_dir = env.subst("$BUILD_DIR") | ||||
|     ld_dir = os.path.join(build_dir, "ld") | ||||
|     if os.path.exists(ld_dir): | ||||
|         local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld") | ||||
|         if os.path.exists(local_ld): | ||||
|             patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld") | ||||
|  | ||||
|  | ||||
| # Check if we're in testing mode | ||||
| build_flags = env.get("BUILD_FLAGS", []) | ||||
| testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) | ||||
|  | ||||
| if testing_mode: | ||||
|     # Create a custom linker script in the build directory with patched memory limits | ||||
|     # This allows larger IRAM/DRAM/Flash for CI component grouping tests | ||||
|     build_dir = env.subst("$BUILD_DIR") | ||||
|     ldscript = env.GetProjectOption("board_build.ldscript", "") | ||||
|     assert ldscript, "No linker script configured in board_build.ldscript" | ||||
|  | ||||
|     framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266") | ||||
|     assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package" | ||||
|  | ||||
|     # Read the original SDK linker script (read-only, SDK is never modified) | ||||
|     sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript) | ||||
|     # Create a custom version in the build directory (isolated, temporary) | ||||
|     custom_ld = os.path.join(build_dir, f"testing_{ldscript}") | ||||
|  | ||||
|     if os.path.exists(sdk_ld) and not os.path.exists(custom_ld): | ||||
|         # Read the SDK linker script | ||||
|         with open(sdk_ld, "r") as f: | ||||
|             content = f.read() | ||||
|  | ||||
|         # Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB) | ||||
|         patched_content = apply_memory_patches(content) | ||||
|  | ||||
|         # Write the patched linker script to the build directory | ||||
|         with open(custom_ld, "w") as f: | ||||
|             f.write(patched_content) | ||||
|  | ||||
|         print(f"ESPHome: Created custom linker script: {custom_ld}") | ||||
|  | ||||
|     # Tell the linker to use our custom script from the build directory | ||||
|     assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}" | ||||
|     env.Replace(LDSCRIPT_PATH=custom_ld) | ||||
|     print(f"ESPHome: Using custom linker script with patched memory limits") | ||||
|  | ||||
|     # Also patch local.eagle.app.v6.common.ld after PlatformIO creates it | ||||
|     env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script) | ||||
| @@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente | ||||
|   TEMPLATABLE_VALUE(std::vector<uint8_t>, data); | ||||
|  | ||||
|  public: | ||||
|   void add_on_sent(const std::vector<Action<Ts...> *> &actions) { | ||||
|   void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) { | ||||
|     this->sent_.add_actions(actions); | ||||
|     if (this->flags_.wait_for_sent) { | ||||
|       this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); })); | ||||
|     } | ||||
|   } | ||||
|   void add_on_error(const std::vector<Action<Ts...> *> &actions) { | ||||
|   void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) { | ||||
|     this->error_.add_actions(actions); | ||||
|     if (this->flags_.wait_for_sent) { | ||||
|       this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) { | ||||
|   | ||||
| @@ -32,6 +32,7 @@ from esphome.const import ( | ||||
|     CONF_MISO_PIN, | ||||
|     CONF_MODE, | ||||
|     CONF_MOSI_PIN, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PAGE_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_POLLING_INTERVAL, | ||||
| @@ -52,12 +53,36 @@ from esphome.core import ( | ||||
|     coroutine_with_priority, | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| CONFLICTS_WITH = ["wifi"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
| AUTO_LOAD = ["network"] | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # RMII pins that are hardcoded on ESP32 classic and cannot be changed | ||||
| # These pins are used by the internal Ethernet MAC when using RMII PHYs | ||||
| ESP32_RMII_FIXED_PINS = { | ||||
|     19: "EMAC_TXD0", | ||||
|     21: "EMAC_TX_EN", | ||||
|     22: "EMAC_TXD1", | ||||
|     25: "EMAC_RXD0", | ||||
|     26: "EMAC_RXD1", | ||||
|     27: "EMAC_RX_CRS_DV", | ||||
| } | ||||
|  | ||||
| # RMII default pins for ESP32-P4 | ||||
| # These are the default pins used by ESP-IDF and are configurable in principle, | ||||
| # but ESPHome's ethernet component currently has no way to change them | ||||
| ESP32P4_RMII_DEFAULT_PINS = { | ||||
|     34: "EMAC_TXD0", | ||||
|     35: "EMAC_TXD1", | ||||
|     28: "EMAC_RX_CRS_DV", | ||||
|     29: "EMAC_RXD0", | ||||
|     30: "EMAC_RXD1", | ||||
|     49: "EMAC_TX_EN", | ||||
| } | ||||
|  | ||||
| ethernet_ns = cg.esphome_ns.namespace("ethernet") | ||||
| PHYRegister = ethernet_ns.struct("PHYRegister") | ||||
| CONF_PHY_ADDR = "phy_addr" | ||||
| @@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate(config): | ||||
| def _final_validate_spi(config): | ||||
|     if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: | ||||
|         return | ||||
|     if spi_configs := fv.full_config.get().get(CONF_SPI): | ||||
| @@ -292,9 +317,6 @@ def _final_validate(config): | ||||
|                     ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
|  | ||||
| def manual_ip(config): | ||||
|     return cg.StructInitializer( | ||||
|         ManualIP, | ||||
| @@ -383,3 +405,57 @@ async def to_code(config): | ||||
|  | ||||
|     if CORE.using_arduino: | ||||
|         cg.add_library("WiFi", None) | ||||
|  | ||||
|  | ||||
| def _final_validate_rmii_pins(config: ConfigType) -> None: | ||||
|     """Validate that RMII pins are not used by other components.""" | ||||
|     # Only validate for RMII-based PHYs on ESP32/ESP32P4 | ||||
|     if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": | ||||
|         return  # SPI and OPENETH don't use RMII | ||||
|  | ||||
|     variant = get_esp32_variant() | ||||
|     if variant == VARIANT_ESP32: | ||||
|         rmii_pins = ESP32_RMII_FIXED_PINS | ||||
|         is_configurable = False | ||||
|     elif variant == VARIANT_ESP32P4: | ||||
|         rmii_pins = ESP32P4_RMII_DEFAULT_PINS | ||||
|         is_configurable = True | ||||
|     else: | ||||
|         return  # No RMII validation needed for other variants | ||||
|  | ||||
|     # Check all used pins against RMII reserved pins | ||||
|     for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): | ||||
|         for pin_path, _, pin_config in pin_list: | ||||
|             pin_num = pin_config.get(CONF_NUMBER) | ||||
|             if pin_num not in rmii_pins: | ||||
|                 continue | ||||
|             # Found a conflict - show helpful error message | ||||
|             pin_function = rmii_pins[pin_num] | ||||
|             component_path = ".".join(str(p) for p in pin_path) | ||||
|             if is_configurable: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is used by Ethernet RMII " | ||||
|                     f"({pin_function}) with the current default " | ||||
|                     f"configuration. This conflicts with '{component_path}'. " | ||||
|                     f"Please choose a different GPIO pin for " | ||||
|                     f"'{component_path}'." | ||||
|                 ) | ||||
|             else: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is reserved for Ethernet RMII " | ||||
|                     f"({pin_function}) and cannot be used. This pin is " | ||||
|                     f"hardcoded by ESP-IDF and cannot be changed when using " | ||||
|                     f"RMII Ethernet PHYs. Please choose a different GPIO pin " | ||||
|                     f"for '{component_path}'." | ||||
|                 ) | ||||
|             raise cv.Invalid(error_msg, path=pin_path) | ||||
|  | ||||
|  | ||||
| def _final_validate(config: ConfigType) -> ConfigType: | ||||
|     """Final validation for Ethernet component.""" | ||||
|     _final_validate_spi(config) | ||||
|     _final_validate_rmii_pins(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|   | ||||
| @@ -8,12 +8,19 @@ namespace event { | ||||
| static const char *const TAG = "event"; | ||||
|  | ||||
| void Event::trigger(const std::string &event_type) { | ||||
|   auto found = types_.find(event_type); | ||||
|   if (found == types_.end()) { | ||||
|   // Linear search - faster than std::set for small datasets (1-5 items typical) | ||||
|   const std::string *found = nullptr; | ||||
|   for (const auto &type : this->types_) { | ||||
|     if (type == event_type) { | ||||
|       found = &type; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (found == nullptr) { | ||||
|     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   last_event_type = &(*found); | ||||
|   last_event_type = found; | ||||
|   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); | ||||
|   this->event_callback_.call(event_type); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <string> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| @@ -26,13 +25,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass { | ||||
|   const std::string *last_event_type; | ||||
|  | ||||
|   void trigger(const std::string &event_type); | ||||
|   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; } | ||||
|   std::set<std::string> get_event_types() const { return this->types_; } | ||||
|   void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; } | ||||
|   const FixedVector<std::string> &get_event_types() const { return this->types_; } | ||||
|   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   CallbackManager<void(const std::string &event_type)> event_callback_; | ||||
|   std::set<std::string> types_; | ||||
|   FixedVector<std::string> types_; | ||||
| }; | ||||
|  | ||||
| }  // namespace event | ||||
|   | ||||
| @@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) { | ||||
|   this->pin_->digital_write(state); | ||||
|   this->publish_state(state); | ||||
| } | ||||
| void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; } | ||||
| void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } | ||||
|  | ||||
| }  // namespace gpio | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -2,10 +2,9 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/switch/switch.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gpio { | ||||
|  | ||||
| @@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component { | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void set_interlock(const std::vector<Switch *> &interlock); | ||||
|   void set_interlock(const std::initializer_list<Switch *> &interlock); | ||||
|   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool state) override; | ||||
|  | ||||
|   GPIOPin *pin_; | ||||
|   std::vector<Switch *> interlock_; | ||||
|   FixedVector<Switch *> interlock_; | ||||
|   uint32_t interlock_wait_time_{0}; | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/hdc2010/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/hdc2010/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@optimusprimespace", "@ssieb"] | ||||
							
								
								
									
										111
									
								
								esphome/components/hdc2010/hdc2010.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								esphome/components/hdc2010/hdc2010.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include "hdc2010.h" | ||||
| // https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js | ||||
| // https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| static const char *const TAG = "hdc2010"; | ||||
|  | ||||
| static const uint8_t HDC2010_ADDRESS = 0x40;  // 0b1000000 or 0b1000001 from datasheet | ||||
| static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; | ||||
| static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; | ||||
| static const uint8_t CONFIG = 0x0E; | ||||
| static const uint8_t MEASUREMENT_CONFIG = 0x0F; | ||||
|  | ||||
| void HDC2010Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   const uint8_t data[2] = { | ||||
|       0b00000000,  // resolution 14bit for both humidity and temperature | ||||
|       0b00000000   // reserved | ||||
|   }; | ||||
|  | ||||
|   if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { | ||||
|     ESP_LOGW(TAG, "Initial config instruction error"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set measurement mode to temperature and humidity | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|   config_contents = (config_contents & 0xF9);  // Always set to TEMP_AND_HUMID mode | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set rate to manual | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x8F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set temperature resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x3F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set humidity resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0xCF; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HDC2010:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::update() { | ||||
|   // Trigger measurement | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents |= 0x01; | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // 1ms delay after triggering the sample | ||||
|   set_timeout(1, [this]() { | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       float temp = this->read_temp(); | ||||
|       this->temperature_sensor_->publish_state(temp); | ||||
|       ESP_LOGD(TAG, "Temp=%.1f°C", temp); | ||||
|     } | ||||
|  | ||||
|     if (this->humidity_sensor_ != nullptr) { | ||||
|       float humidity = this->read_humidity(); | ||||
|       this->humidity_sensor_->publish_state(humidity); | ||||
|       ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_temp() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t temp = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) temp * 0.0025177f - 40.0f; | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_humidity() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t humidity = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) humidity * 0.001525879f; | ||||
| } | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										32
									
								
								esphome/components/hdc2010/hdc2010.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/hdc2010/hdc2010.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| class HDC2010Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } | ||||
|  | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } | ||||
|  | ||||
|   /// Setup the sensor and check for connection. | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   /// Retrieve the latest sensor values. This operation takes approximately 16ms. | ||||
|   void update() override; | ||||
|  | ||||
|   float read_temp(); | ||||
|  | ||||
|   float read_humidity(); | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										56
									
								
								esphome/components/hdc2010/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/hdc2010/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| hdc2010_ns = cg.esphome_ns.namespace("hdc2010") | ||||
| HDC2010Component = hdc2010_ns.class_( | ||||
|     "HDC2010Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(HDC2010Component), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x40)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if temperature_config := config.get(CONF_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(temperature_config) | ||||
|         cg.add(var.set_temperature_sensor(sens)) | ||||
|  | ||||
|     if humidity_config := config.get(CONF_HUMIDITY): | ||||
|         sens = await sensor.new_sensor(humidity_config) | ||||
|         cg.add(var.set_humidity_sensor(sens)) | ||||
| @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace host | ||||
|   | ||||
| @@ -57,6 +57,9 @@ async def host_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(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)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -169,7 +169,7 @@ class HttpRequestComponent : public Component { | ||||
|  protected: | ||||
|   virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, | ||||
|                                                  const std::string &body, const std::list<Header> &request_headers, | ||||
|                                                  std::set<std::string> collect_headers) = 0; | ||||
|                                                  const std::set<std::string> &collect_headers) = 0; | ||||
|   const char *useragent_{nullptr}; | ||||
|   bool follow_redirects_{}; | ||||
|   uint16_t redirect_limit_{}; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method, | ||||
|                                                            const std::string &body, | ||||
|                                                            const std::list<Header> &request_headers, | ||||
|                                                            std::set<std::string> collect_headers) { | ||||
|                                                            const std::set<std::string> &collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace http_request | ||||
|   | ||||
| @@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method, | ||||
|                                                         const std::string &body, | ||||
|                                                         const std::list<Header> &request_headers, | ||||
|                                                         std::set<std::string> response_headers) { | ||||
|                                                         const std::set<std::string> &response_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { | ||||
|  public: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          std::set<std::string> response_headers) override; | ||||
|                                          const std::set<std::string> &response_headers) override; | ||||
|   void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { | ||||
| std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method, | ||||
|                                                        const std::string &body, | ||||
|                                                        const std::list<Header> &request_headers, | ||||
|                                                        std::set<std::string> collect_headers) { | ||||
|                                                        const std::set<std::string> &collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
|   // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE | ||||
|   uint16_t buffer_size_rx_{}; | ||||
|   uint16_t buffer_size_tx_{}; | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import re | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import __version__ | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| @@ -35,7 +37,9 @@ def _process_next_url(url: str): | ||||
|     return url | ||||
|  | ||||
|  | ||||
| async def setup_improv_core(var, config): | ||||
|     if CONF_NEXT_URL in config: | ||||
|         cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) | ||||
| async def setup_improv_core(var: MockObj, config: ConfigType, component: str): | ||||
|     if next_url := config.get(CONF_NEXT_URL): | ||||
|         cg.add(var.set_next_url(_process_next_url(next_url))) | ||||
|         cg.add_define(f"USE_{component.upper()}_NEXT_URL") | ||||
|  | ||||
|     cg.add_library("improv/Improv", "1.2.4") | ||||
|   | ||||
| @@ -2,10 +2,12 @@ | ||||
|  | ||||
| #include "esphome/components/network/util.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace improv_base { | ||||
|  | ||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) | ||||
| static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}"; | ||||
| static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1; | ||||
| static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}"; | ||||
| @@ -43,6 +45,7 @@ std::string ImprovBase::get_formatted_next_url_() { | ||||
|  | ||||
|   return formatted_url; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace improv_base | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,17 +1,22 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <string> | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace improv_base { | ||||
|  | ||||
| class ImprovBase { | ||||
|  public: | ||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) | ||||
|   void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) | ||||
|   std::string get_formatted_next_url_(); | ||||
|   std::string next_url_; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace improv_base | ||||
|   | ||||
| @@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await improv_base.setup_improv_core(var, config) | ||||
|     await improv_base.setup_improv_core(var, config, "improv_serial") | ||||
|   | ||||
| @@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } | ||||
|  | ||||
| optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
| @@ -78,8 +110,28 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
|   return byte; | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { | ||||
|   data.push_back('\n'); | ||||
| void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { | ||||
|   // First, set length field | ||||
|   this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; | ||||
|  | ||||
|   const bool there_is_data = data != nullptr && size > 0; | ||||
|   // If there_is_data, checksum must not include our optional data byte | ||||
|   const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; | ||||
|   // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) | ||||
|   const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; | ||||
|   // Calculate checksum for message | ||||
|   uint8_t checksum = 0; | ||||
|   for (uint8_t i = 0; i < header_checksum_len; i++) { | ||||
|     checksum += this->tx_header_[i]; | ||||
|   } | ||||
|   if (there_is_data) { | ||||
|     // Include data in checksum | ||||
|     for (size_t i = 0; i < size; i++) { | ||||
|       checksum += data[i]; | ||||
|     } | ||||
|   } | ||||
|   this->tx_header_[TX_CHECKSUM_IDX] = checksum; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   switch (logger::global_logger->get_uart()) { | ||||
|     case logger::UART_SELECTION_UART0: | ||||
| @@ -87,68 +139,52 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ | ||||
|     !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||
|     case logger::UART_SELECTION_UART2: | ||||
| #endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 | ||||
|       uart_write_bytes(this->uart_num_, data.data(), data.size()); | ||||
| #endif | ||||
|       uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         uart_write_bytes(this->uart_num_, data, size); | ||||
|         uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|       } | ||||
|       break; | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) | ||||
|     case logger::UART_SELECTION_USB_CDC: { | ||||
|       const char *msg = (char *) data.data(); | ||||
|       esp_usb_console_write_buf(msg, data.size()); | ||||
|     case logger::UART_SELECTION_USB_CDC: | ||||
|       esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         esp_usb_console_write_buf((const char *) data, size); | ||||
|         esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], | ||||
|                                   2);  // Footer: checksum and newline | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
| #endif  // USE_LOGGER_USB_CDC | ||||
| #endif | ||||
| #ifdef USE_LOGGER_USB_SERIAL_JTAG | ||||
|     case logger::UART_SELECTION_USB_SERIAL_JTAG: | ||||
|       usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); | ||||
|       delay(10); | ||||
|       usb_serial_jtag_ll_txfifo_flush();  // fixes for issue in IDF 4.4.7 | ||||
|       usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); | ||||
|       if (there_is_data) { | ||||
|         usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); | ||||
|         usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, | ||||
|                                     20 / portTICK_PERIOD_MS);  // Footer: checksum and newline | ||||
|       } | ||||
|       break; | ||||
| #endif  // USE_LOGGER_USB_SERIAL_JTAG | ||||
| #endif | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| #elif defined(USE_ARDUINO) | ||||
|   this->hw_serial_->write(data.data(), data.size()); | ||||
|   this->hw_serial_->write(this->tx_header_, header_tx_len); | ||||
|   if (there_is_data) { | ||||
|     this->hw_serial_->write(data, size); | ||||
|     this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Improv Serial timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | ||||
|   std::vector<std::string> urls; | ||||
| #ifdef USE_IMPROV_SERIAL_NEXT_URL | ||||
|   if (!this->next_url_.empty()) { | ||||
|     urls.push_back(this->get_formatted_next_url_()); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_WEBSERVER | ||||
|   for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { | ||||
|     if (ip.is_ip4()) { | ||||
| @@ -175,13 +211,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() { | ||||
| bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { | ||||
|   size_t at = this->rx_buffer_.size(); | ||||
|   this->rx_buffer_.push_back(byte); | ||||
|   ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); | ||||
|   ESP_LOGV(TAG, "Byte: 0x%02X", byte); | ||||
|   const uint8_t *raw = &this->rx_buffer_[0]; | ||||
|  | ||||
|   return improv::parse_improv_serial_byte( | ||||
|       at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, | ||||
|       [this](improv::Error error) -> void { | ||||
|         ESP_LOGW(TAG, "Error decoding Improv payload"); | ||||
|         ESP_LOGW(TAG, "Error decoding payload"); | ||||
|         this->set_error_(error); | ||||
|       }); | ||||
| } | ||||
| @@ -197,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       wifi::global_wifi_component->set_sta(sta); | ||||
|       wifi::global_wifi_component->start_connecting(sta, false); | ||||
|       this->set_state_(improv::STATE_PROVISIONING); | ||||
|       ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|       ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|                command.password.c_str()); | ||||
|  | ||||
|       auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); | ||||
| @@ -238,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       return true; | ||||
|     } | ||||
|     default: { | ||||
|       ESP_LOGW(TAG, "Unknown Improv payload"); | ||||
|       ESP_LOGW(TAG, "Unknown payload"); | ||||
|       this->set_error_(improv::ERROR_UNKNOWN_RPC); | ||||
|       return false; | ||||
|     } | ||||
| @@ -247,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|  | ||||
| void ImprovSerialComponent::set_state_(improv::State state) { | ||||
|   this->state_ = state; | ||||
|  | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_CURRENT_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = state; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|  | ||||
|   this->write_data_(data); | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = state; | ||||
|   this->write_data_(); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::set_error_(improv::Error error) { | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_ERROR_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = error; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|   this->write_data_(data); | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = error; | ||||
|   this->write_data_(); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) { | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(9); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_RPC_RESPONSE; | ||||
|   data[8] = response.size(); | ||||
|   data.insert(data.end(), response.begin(), response.end()); | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data.push_back(checksum); | ||||
|  | ||||
|   this->write_data_(data); | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; | ||||
|   this->write_data_(response.data(), response.size()); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::on_wifi_connect_timeout_() { | ||||
|   this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); | ||||
|   this->set_state_(improv::STATE_AUTHORIZED); | ||||
|   ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); | ||||
|   ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); | ||||
|   wifi::global_wifi_component->clear_sta(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,16 @@ | ||||
| namespace esphome { | ||||
| namespace improv_serial { | ||||
|  | ||||
| // TX buffer layout constants | ||||
| static constexpr uint8_t TX_HEADER_SIZE = 6;  // Bytes 0-5 = "IMPROV" | ||||
| static constexpr uint8_t TX_VERSION_IDX = 6; | ||||
| static constexpr uint8_t TX_TYPE_IDX = 7; | ||||
| static constexpr uint8_t TX_LENGTH_IDX = 8; | ||||
| static constexpr uint8_t TX_DATA_IDX = 9;  // For state/error messages only | ||||
| static constexpr uint8_t TX_CHECKSUM_IDX = 10; | ||||
| static constexpr uint8_t TX_NEWLINE_IDX = 11; | ||||
| static constexpr uint8_t TX_BUFFER_SIZE = 12; | ||||
|  | ||||
| enum ImprovSerialType : uint8_t { | ||||
|   TYPE_CURRENT_STATE = 0x01, | ||||
|   TYPE_ERROR_STATE = 0x02, | ||||
| @@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { | ||||
|   std::vector<uint8_t> build_version_info_(); | ||||
|  | ||||
|   optional<uint8_t> read_byte_(); | ||||
|   void write_data_(std::vector<uint8_t> &data); | ||||
|   void write_data_(const uint8_t *data = nullptr, size_t size = 0); | ||||
|  | ||||
|   uint8_t tx_header_[TX_BUFFER_SIZE] = { | ||||
|       'I',                    // 0: Header | ||||
|       'M',                    // 1: Header | ||||
|       'P',                    // 2: Header | ||||
|       'R',                    // 3: Header | ||||
|       'O',                    // 4: Header | ||||
|       'V',                    // 5: Header | ||||
|       IMPROV_SERIAL_VERSION,  // 6: Version | ||||
|       0,                      // 7: ImprovSerialType | ||||
|       0,                      // 8: Length | ||||
|       0,                      // 9...X: Data (here, one byte reserved for state/error) | ||||
|       0,                      // X + 10: Checksum | ||||
|       '\n', | ||||
|   }; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   uart_port_t uart_num_; | ||||
|   | ||||
| @@ -199,6 +199,9 @@ async def component_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(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)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace libretiny | ||||
|   | ||||
| @@ -61,8 +61,12 @@ void AddressableLightTransformer::start() { | ||||
|   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); | ||||
| } | ||||
|  | ||||
| inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { | ||||
|   return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256)); | ||||
| } | ||||
|  | ||||
| optional<LightColorValues> AddressableLightTransformer::apply() { | ||||
|   float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_()); | ||||
|   float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); | ||||
|  | ||||
|   // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the | ||||
|   // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the | ||||
| @@ -74,38 +78,37 @@ optional<LightColorValues> AddressableLightTransformer::apply() { | ||||
|   // all LEDs, we use the current state of each LED as the start. | ||||
|  | ||||
|   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original | ||||
|   // state of each LED at the start of the transition. | ||||
|   // Instead, we "fake" the look of the LERP by using an exponential average over time and using | ||||
|   // dynamically-calculated alpha values to match the look. | ||||
|   // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating | ||||
|   // the delta between the current state and the target state, assuming that the delta represents the rest | ||||
|   // of the transition that was to be applied as of the previous transition step, and scaling the delta for | ||||
|   // what should be left after the current transition step. In this manner, the delta decays to zero as the | ||||
|   // transition progresses. | ||||
|   // | ||||
|   // Here's an example of how the algorithm progresses in discrete steps: | ||||
|   // | ||||
|   // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%. | ||||
|   // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%. | ||||
|   // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%. | ||||
|   // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%. | ||||
|   // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%. | ||||
|   // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%. | ||||
|   // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%. | ||||
|   // | ||||
|   // Because the color values are quantized to 8 bit resolution after each step, the transition may appear | ||||
|   // non-linear when applying small deltas. | ||||
|  | ||||
|   float denom = (1.0f - smoothed_progress); | ||||
|   float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; | ||||
|  | ||||
|   // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length | ||||
|   // We solve this by accumulating the fractional part of the alpha over time. | ||||
|   float alpha255 = alpha * 255.0f; | ||||
|   float alpha255int = floorf(alpha255); | ||||
|   float alpha255remainder = alpha255 - alpha255int; | ||||
|  | ||||
|   this->accumulated_alpha_ += alpha255remainder; | ||||
|   float alpha_add = floorf(this->accumulated_alpha_); | ||||
|   this->accumulated_alpha_ -= alpha_add; | ||||
|  | ||||
|   alpha255 += alpha_add; | ||||
|   alpha255 = clamp(alpha255, 0.0f, 255.0f); | ||||
|   auto alpha8 = static_cast<uint8_t>(alpha255); | ||||
|  | ||||
|   if (alpha8 != 0) { | ||||
|     uint8_t inv_alpha8 = 255 - alpha8; | ||||
|     Color add = this->target_color_ * alpha8; | ||||
|  | ||||
|     for (auto led : this->light_) | ||||
|       led.set(add + led.get() * inv_alpha8); | ||||
|   if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { | ||||
|     int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); | ||||
|     for (auto led : this->light_) { | ||||
|       led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); | ||||
|     } | ||||
|     this->last_transition_progress_ = smoothed_progress; | ||||
|     this->light_.schedule_show(); | ||||
|   } | ||||
|  | ||||
|   this->last_transition_progress_ = smoothed_progress; | ||||
|   this->light_.schedule_show(); | ||||
|  | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| #include "esphome/core/defines.h" | ||||
| #include "light_output.h" | ||||
| #include "light_state.h" | ||||
| #include "transformers.h" | ||||
| #include "light_transformer.h" | ||||
|  | ||||
| #ifdef USE_POWER_SUPPLY | ||||
| #include "esphome/components/power_supply/power_supply.h" | ||||
| @@ -103,7 +103,7 @@ class AddressableLight : public LightOutput, public Component { | ||||
|   bool effect_active_{false}; | ||||
| }; | ||||
|  | ||||
| class AddressableLightTransformer : public LightTransitionTransformer { | ||||
| class AddressableLightTransformer : public LightTransformer { | ||||
|  public: | ||||
|   AddressableLightTransformer(AddressableLight &light) : light_(light) {} | ||||
|  | ||||
| @@ -113,7 +113,6 @@ class AddressableLightTransformer : public LightTransitionTransformer { | ||||
|  protected: | ||||
|   AddressableLight &light_; | ||||
|   float last_transition_progress_{0.0f}; | ||||
|   float accumulated_alpha_{0.0f}; | ||||
|   Color target_color_{}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/light/light_state.h" | ||||
| #include "esphome/components/light/addressable_light.h" | ||||
|  | ||||
| @@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) | ||||
|  | ||||
| class AddressableLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   explicit AddressableLightEffect(const char *name) : LightEffect(name) {} | ||||
|   void start_internal() override { | ||||
|     this->get_addressable_()->set_effect_active(true); | ||||
|     this->get_addressable_()->clear_effect_data(); | ||||
| @@ -57,8 +57,7 @@ class AddressableLightEffect : public LightEffect { | ||||
|  | ||||
| class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   AddressableLambdaLightEffect(const std::string &name, | ||||
|                                std::function<void(AddressableLight &, Color, bool initial_run)> f, | ||||
|   AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f, | ||||
|                                uint32_t update_interval) | ||||
|       : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|   void start() override { this->initial_run_ = true; } | ||||
| @@ -81,7 +80,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableRainbowLightEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     ESPHSVColor hsv; | ||||
|     hsv.value = 255; | ||||
| @@ -112,8 +111,8 @@ struct AddressableColorWipeEffectColor { | ||||
|  | ||||
| class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void set_colors(const std::vector<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } | ||||
|   explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } | ||||
|   void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } | ||||
|   void set_reverse(bool reverse) { this->reverse_ = reverse; } | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
| @@ -155,7 +154,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   std::vector<AddressableColorWipeEffectColor> colors_; | ||||
|   FixedVector<AddressableColorWipeEffectColor> colors_; | ||||
|   size_t at_color_{0}; | ||||
|   uint32_t last_add_{0}; | ||||
|   uint32_t add_led_interval_{}; | ||||
| @@ -165,7 +164,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableScanEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } | ||||
|   void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
| @@ -202,7 +201,7 @@ class AddressableScanEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableTwinkleEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &addressable, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     uint8_t pos_add = 0; | ||||
| @@ -244,7 +243,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableRandomTwinkleEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     uint8_t pos_add = 0; | ||||
| @@ -293,7 +292,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableFireworksEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void start() override { | ||||
|     auto &it = *this->get_addressable_(); | ||||
|     it.all() = Color::BLACK; | ||||
| @@ -342,7 +341,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableFlickerEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     const uint8_t intensity = this->intensity_; | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "light_effect.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -17,7 +17,7 @@ inline static float random_cubic_float() { | ||||
| /// Pulse effect. | ||||
| class PulseLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   explicit PulseLightEffect(const char *name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
| @@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect { | ||||
| /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. | ||||
| class RandomLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit RandomLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   explicit RandomLightEffect(const char *name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
| @@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect { | ||||
|  | ||||
| class LambdaLightEffect : public LightEffect { | ||||
|  public: | ||||
|   LambdaLightEffect(const std::string &name, std::function<void(bool initial_run)> f, uint32_t update_interval) | ||||
|   LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval) | ||||
|       : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|  | ||||
|   void start() override { this->initial_run_ = true; } | ||||
| @@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect { | ||||
|  | ||||
| class AutomationLightEffect : public LightEffect { | ||||
|  public: | ||||
|   AutomationLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   AutomationLightEffect(const char *name) : LightEffect(name) {} | ||||
|   void stop() override { this->trig_->stop_action(); } | ||||
|   void apply() override { | ||||
|     if (!this->trig_->is_action_running()) { | ||||
| @@ -163,7 +163,7 @@ struct StrobeLightEffectColor { | ||||
|  | ||||
| class StrobeLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   explicit StrobeLightEffect(const char *name) : LightEffect(name) {} | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
|     if (now - this->last_switch_ < this->colors_[this->at_color_].duration) | ||||
| @@ -188,17 +188,17 @@ class StrobeLightEffect : public LightEffect { | ||||
|     this->last_switch_ = now; | ||||
|   } | ||||
|  | ||||
|   void set_colors(const std::vector<StrobeLightEffectColor> &colors) { this->colors_ = colors; } | ||||
|   void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; } | ||||
|  | ||||
|  protected: | ||||
|   std::vector<StrobeLightEffectColor> colors_; | ||||
|   FixedVector<StrobeLightEffectColor> colors_; | ||||
|   uint32_t last_switch_{0}; | ||||
|   size_t at_color_{0}; | ||||
| }; | ||||
|  | ||||
| class FlickerLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   explicit FlickerLightEffect(const char *name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     LightColorValues remote = this->state_->remote_values; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace light { | ||||
| @@ -107,13 +108,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { | ||||
| // Type alias for raw color mode bitmask values | ||||
| using color_mode_bitmask_t = uint16_t; | ||||
|  | ||||
| // Constants for ColorMode count and bit range | ||||
| static constexpr int COLOR_MODE_COUNT = 10;                             // UNKNOWN through RGB_COLD_WARM_WHITE | ||||
| static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8;  // Number of bits in bitmask type | ||||
|  | ||||
| // Compile-time array of all ColorMode values in declaration order | ||||
| // Bit positions (0-9) map directly to enum declaration order | ||||
| static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { | ||||
| // Lookup table for ColorMode bit mapping | ||||
| // This array defines the canonical order of color modes (bit 0-9) | ||||
| constexpr ColorMode COLOR_MODE_LOOKUP[] = { | ||||
|     ColorMode::UNKNOWN,                // bit 0 | ||||
|     ColorMode::ON_OFF,                 // bit 1 | ||||
|     ColorMode::BRIGHTNESS,             // bit 2 | ||||
| @@ -126,33 +123,42 @@ static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { | ||||
|     ColorMode::RGB_COLD_WARM_WHITE,    // bit 9 | ||||
| }; | ||||
|  | ||||
| /// Map ColorMode enum values to bit positions (0-9) | ||||
| /// Bit positions follow the enum declaration order | ||||
| static constexpr int mode_to_bit(ColorMode mode) { | ||||
|   // Linear search through COLOR_MODES array | ||||
|   // Compiler optimizes this to efficient code since array is constexpr | ||||
|   for (int i = 0; i < COLOR_MODE_COUNT; ++i) { | ||||
|     if (COLOR_MODES[i] == mode) | ||||
|       return i; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
| /// Bit mapping policy for ColorMode | ||||
| /// Uses lookup table for non-contiguous enum values | ||||
| struct ColorModeBitPolicy { | ||||
|   using mask_t = uint16_t;  // 10 bits requires uint16_t | ||||
|   static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); | ||||
|  | ||||
| /// Map bit positions (0-9) to ColorMode enum values | ||||
| /// Bit positions follow the enum declaration order | ||||
| static constexpr ColorMode bit_to_mode(int bit) { | ||||
|   // Direct lookup in COLOR_MODES array | ||||
|   return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; | ||||
| } | ||||
|   static constexpr unsigned to_bit(ColorMode mode) { | ||||
|     // Linear search through lookup table | ||||
|     // Compiler optimizes this to efficient code since array is constexpr | ||||
|     for (int i = 0; i < MAX_BITS; ++i) { | ||||
|       if (COLOR_MODE_LOOKUP[i] == mode) | ||||
|         return i; | ||||
|     } | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   static constexpr ColorMode from_bit(unsigned bit) { | ||||
|     return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Type alias for ColorMode bitmask using policy-based design | ||||
| using ColorModeMask = FiniteSetMask<ColorMode, ColorModeBitPolicy>; | ||||
|  | ||||
| // Number of ColorCapability enum values | ||||
| constexpr int COLOR_CAPABILITY_COUNT = 6; | ||||
|  | ||||
| /// Helper to compute capability bitmask at compile time | ||||
| static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { | ||||
|   color_mode_bitmask_t mask = 0; | ||||
| constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { | ||||
|   uint16_t mask = 0; | ||||
|   uint8_t cap_bit = static_cast<uint8_t>(capability); | ||||
|  | ||||
|   // Check each ColorMode to see if it has this capability | ||||
|   for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { | ||||
|     uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit)); | ||||
|   constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); | ||||
|   for (int bit = 0; bit < color_mode_count; ++bit) { | ||||
|     uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]); | ||||
|     if ((mode_val & cap_bit) != 0) { | ||||
|       mask |= (1 << bit); | ||||
|     } | ||||
| @@ -160,12 +166,9 @@ static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability | ||||
|   return mask; | ||||
| } | ||||
|  | ||||
| // Number of ColorCapability enum values | ||||
| static constexpr int COLOR_CAPABILITY_COUNT = 6; | ||||
|  | ||||
| /// Compile-time lookup table mapping ColorCapability to bitmask | ||||
| /// This array is computed at compile time using constexpr | ||||
| static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { | ||||
| constexpr uint16_t CAPABILITY_BITMASKS[] = { | ||||
|     compute_capability_bitmask(ColorCapability::ON_OFF),             // 1 << 0 | ||||
|     compute_capability_bitmask(ColorCapability::BRIGHTNESS),         // 1 << 1 | ||||
|     compute_capability_bitmask(ColorCapability::WHITE),              // 1 << 2 | ||||
| @@ -174,130 +177,38 @@ static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { | ||||
|     compute_capability_bitmask(ColorCapability::RGB),                // 1 << 5 | ||||
| }; | ||||
|  | ||||
| /// Bitmask for storing a set of ColorMode values efficiently. | ||||
| /// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes). | ||||
| class ColorModeMask { | ||||
|  public: | ||||
|   constexpr ColorModeMask() = default; | ||||
|  | ||||
|   /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} | ||||
|   constexpr ColorModeMask(std::initializer_list<ColorMode> modes) { | ||||
|     for (auto mode : modes) { | ||||
|       this->add(mode); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } | ||||
|  | ||||
|   /// Add multiple modes at once using initializer list | ||||
|   constexpr void add(std::initializer_list<ColorMode> modes) { | ||||
|     for (auto mode : modes) { | ||||
|       this->add(mode); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } | ||||
|  | ||||
|   constexpr size_t size() const { | ||||
|     // Count set bits using Brian Kernighan's algorithm | ||||
|     // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) | ||||
|     uint16_t n = this->mask_; | ||||
|     size_t count = 0; | ||||
|     while (n) { | ||||
|       n &= n - 1;  // Clear the least significant set bit | ||||
|       count++; | ||||
|     } | ||||
|     return count; | ||||
|   } | ||||
|  | ||||
|   constexpr bool empty() const { return this->mask_ == 0; } | ||||
|  | ||||
|   /// Iterator support for API encoding | ||||
|   class Iterator { | ||||
|    public: | ||||
|     using iterator_category = std::forward_iterator_tag; | ||||
|     using value_type = ColorMode; | ||||
|     using difference_type = std::ptrdiff_t; | ||||
|     using pointer = const ColorMode *; | ||||
|     using reference = ColorMode; | ||||
|  | ||||
|     constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } | ||||
|  | ||||
|     constexpr ColorMode operator*() const { return bit_to_mode(bit_); } | ||||
|  | ||||
|     constexpr Iterator &operator++() { | ||||
|       ++bit_; | ||||
|       advance_to_next_set_bit_(); | ||||
|       return *this; | ||||
|     } | ||||
|  | ||||
|     constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } | ||||
|  | ||||
|     constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } | ||||
|  | ||||
|    private: | ||||
|     constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } | ||||
|  | ||||
|     color_mode_bitmask_t mask_; | ||||
|     int bit_; | ||||
|   }; | ||||
|  | ||||
|   constexpr Iterator begin() const { return Iterator(mask_, 0); } | ||||
|   constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } | ||||
|  | ||||
|   /// Get the raw bitmask value for API encoding | ||||
|   constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } | ||||
|  | ||||
|   /// Find the next set bit in a bitmask starting from a given position | ||||
|   /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set | ||||
|   static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { | ||||
|     int bit = start_bit; | ||||
|     while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { | ||||
|       ++bit; | ||||
|     } | ||||
|     return bit; | ||||
|   } | ||||
|  | ||||
|   /// Find the first set bit in a bitmask and return the corresponding ColorMode | ||||
|   /// Used for optimizing compute_color_mode_() intersection logic | ||||
|   static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { | ||||
|     return bit_to_mode(find_next_set_bit(mask, 0)); | ||||
|   } | ||||
|  | ||||
|   /// Check if a ColorMode is present in a raw bitmask value | ||||
|   /// Useful for checking intersection results without creating a temporary ColorModeMask | ||||
|   static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { | ||||
|     return (mask & (1 << mode_to_bit(mode))) != 0; | ||||
|   } | ||||
|  | ||||
|   /// Check if any mode in the bitmask has a specific capability | ||||
|   /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) | ||||
|   bool has_capability(ColorCapability capability) const { | ||||
|     // Lookup the pre-computed bitmask for this capability and check intersection with our mask | ||||
|     // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 | ||||
|     // We need to convert the power-of-2 value to an index | ||||
|     uint8_t cap_val = static_cast<uint8_t>(capability); | ||||
| /** | ||||
|  * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS | ||||
|  * lookup. | ||||
|  * | ||||
|  * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5). | ||||
|  * Used to index into the CAPABILITY_BITMASKS lookup table. | ||||
|  * | ||||
|  * @param capability A ColorCapability enum value (must be a power of 2). | ||||
|  * @return The corresponding array index (0-based). | ||||
|  */ | ||||
| inline int capability_to_index(ColorCapability capability) { | ||||
|   uint8_t cap_val = static_cast<uint8_t>(capability); | ||||
| #if defined(__GNUC__) || defined(__clang__) | ||||
|     // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) | ||||
|     int index = __builtin_ctz(cap_val); | ||||
|   // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) | ||||
|   return __builtin_ctz(cap_val); | ||||
| #else | ||||
|     // Fallback for compilers without __builtin_ctz | ||||
|     int index = 0; | ||||
|     while (cap_val > 1) { | ||||
|       cap_val >>= 1; | ||||
|       ++index; | ||||
|     } | ||||
| #endif | ||||
|     return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; | ||||
|   // Fallback for compilers without __builtin_ctz | ||||
|   int index = 0; | ||||
|   while (cap_val > 1) { | ||||
|     cap_val >>= 1; | ||||
|     ++index; | ||||
|   } | ||||
|   return index; | ||||
| #endif | ||||
| } | ||||
|  | ||||
|  private: | ||||
|   // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). | ||||
|   // Currently only 10 ColorMode values exist, so 16 bits is sufficient. | ||||
|   // Can be changed to uint32_t if more than 16 color modes are needed in the future. | ||||
|   // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). | ||||
|   color_mode_bitmask_t mask_{0}; | ||||
| }; | ||||
| /// Check if any mode in the bitmask has a specific capability | ||||
| /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) | ||||
| inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { | ||||
|   // Lookup the pre-computed bitmask for this capability and check intersection with our mask | ||||
|   return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; | ||||
| } | ||||
|  | ||||
| }  // namespace light | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -17,19 +17,19 @@ class ESPColorCorrection { | ||||
|                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); | ||||
|   } | ||||
|   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); | ||||
|     uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); | ||||
|     uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); | ||||
|     uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); | ||||
|     uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { | ||||
|   | ||||
| @@ -156,7 +156,7 @@ void LightCall::perform() { | ||||
|     if (this->effect_ == 0u) { | ||||
|       effect_s = "None"; | ||||
|     } else { | ||||
|       effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); | ||||
|       effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); | ||||
|     } | ||||
|  | ||||
|     if (publish) { | ||||
| @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { | ||||
|  | ||||
|   // Use the preferred suitable mode. | ||||
|   if (intersection != 0) { | ||||
|     ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); | ||||
|     ColorMode mode = ColorModeMask::first_value_from_mask(intersection); | ||||
|     ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), | ||||
|              LOG_STR_ARG(color_mode_to_human(mode))); | ||||
|     return mode; | ||||
| @@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { | ||||
|   for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { | ||||
|     LightEffect *e = this->parent_->effects_[i]; | ||||
|  | ||||
|     if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { | ||||
|     if (strcasecmp(effect.c_str(), e->get_name()) == 0) { | ||||
|       this->set_effect(i + 1); | ||||
|       found = true; | ||||
|       break; | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -11,7 +9,7 @@ class LightState; | ||||
|  | ||||
| class LightEffect { | ||||
|  public: | ||||
|   explicit LightEffect(std::string name) : name_(std::move(name)) {} | ||||
|   explicit LightEffect(const char *name) : name_(name) {} | ||||
|  | ||||
|   /// Initialize this LightEffect. Will be called once after creation. | ||||
|   virtual void start() {} | ||||
| @@ -24,7 +22,11 @@ class LightEffect { | ||||
|   /// Apply this effect. Use the provided state for starting transitions, ... | ||||
|   virtual void apply() = 0; | ||||
|  | ||||
|   const std::string &get_name() { return this->name_; } | ||||
|   /** | ||||
|    * Returns the name of this effect. | ||||
|    * The returned pointer is valid for the lifetime of the program and must not be freed. | ||||
|    */ | ||||
|   const char *get_name() const { return this->name_; } | ||||
|  | ||||
|   /// Internal method called by the LightState when this light effect is registered in it. | ||||
|   virtual void init() {} | ||||
| @@ -47,7 +49,7 @@ class LightEffect { | ||||
|  | ||||
|  protected: | ||||
|   LightState *state_{nullptr}; | ||||
|   std::string name_; | ||||
|   const char *name_; | ||||
|  | ||||
|   /// Internal method to find this effect's index in the parent light's effect list. | ||||
|   uint32_t get_index_in_parent_() const; | ||||
|   | ||||
| @@ -178,12 +178,9 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore | ||||
| void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } | ||||
| bool LightState::supports_effects() { return !this->effects_.empty(); } | ||||
| const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; } | ||||
| void LightState::add_effects(const std::vector<LightEffect *> &effects) { | ||||
| void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) { | ||||
|   // Called once from Python codegen during setup with all effects from YAML config | ||||
|   this->effects_.init(effects.size()); | ||||
|   for (auto *effect : effects) { | ||||
|     this->effects_.push_back(effect); | ||||
|   } | ||||
|   this->effects_ = effects; | ||||
| } | ||||
|  | ||||
| void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } | ||||
|   | ||||
| @@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component { | ||||
|   const FixedVector<LightEffect *> &get_effects() const; | ||||
|  | ||||
|   /// Add effects for this light state. | ||||
|   void add_effects(const std::vector<LightEffect *> &effects); | ||||
|   void add_effects(const std::initializer_list<LightEffect *> &effects); | ||||
|  | ||||
|   /// Get the total number of effects available for this light. | ||||
|   size_t get_effect_count() const { return this->effects_.size(); } | ||||
| @@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component { | ||||
|       return 0; | ||||
|     } | ||||
|     for (size_t i = 0; i < this->effects_.size(); i++) { | ||||
|       if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) { | ||||
|       if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) { | ||||
|         return i + 1;  // Effects are 1-indexed in active_effect_index_ | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -26,9 +26,9 @@ class LightTraits { | ||||
|     this->supported_color_modes_ = ColorModeMask(modes); | ||||
|   } | ||||
|  | ||||
|   bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } | ||||
|   bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } | ||||
|   bool supports_color_capability(ColorCapability color_capability) const { | ||||
|     return this->supported_color_modes_.has_capability(color_capability); | ||||
|     return has_capability(this->supported_color_modes_, color_capability); | ||||
|   } | ||||
|  | ||||
|   float get_min_mireds() const { return this->min_mireds_; } | ||||
|   | ||||
| @@ -38,6 +38,10 @@ class LightTransformer { | ||||
|   const LightColorValues &get_target_values() const { return this->target_values_; } | ||||
|  | ||||
|  protected: | ||||
|   // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like | ||||
|   // transition from 0 to 1 on x = [0, 1] | ||||
|   static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } | ||||
|  | ||||
|   /// The progress of this transition, on a scale of 0 to 1. | ||||
|   float get_progress_() { | ||||
|     uint32_t now = esphome::millis(); | ||||
|   | ||||
| @@ -50,15 +50,11 @@ class LightTransitionTransformer : public LightTransformer { | ||||
|     if (this->changing_color_mode_) | ||||
|       p = p < 0.5f ? p * 2 : (p - 0.5) * 2; | ||||
|  | ||||
|     float v = LightTransitionTransformer::smoothed_progress(p); | ||||
|     float v = LightTransformer::smoothed_progress(p); | ||||
|     return LightColorValues::lerp(start, end, v); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like | ||||
|   // transition from 0 to 1 on x = [0, 1] | ||||
|   static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } | ||||
|  | ||||
|   LightColorValues end_values_{}; | ||||
|   LightColorValues intermediate_values_{}; | ||||
|   bool changing_color_mode_{false}; | ||||
|   | ||||
| @@ -99,7 +99,11 @@ const std::string &get_use_address() { | ||||
|   return wifi::global_wifi_component->get_use_address(); | ||||
| #endif | ||||
|  | ||||
| #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) | ||||
| #ifdef USE_OPENTHREAD | ||||
|   return openthread::global_openthread_component->get_use_address(); | ||||
| #endif | ||||
|  | ||||
| #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) | ||||
|   // Fallback when no network component is defined (e.g., host platform) | ||||
|   static const std::string empty; | ||||
|   return empty; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import logging | ||||
| from pathlib import Path | ||||
|  | ||||
| @@ -277,3 +278,19 @@ def upload_program(config: ConfigType, args, host: str) -> bool: | ||||
|         raise EsphomeError(f"Upload failed with result: {result}") | ||||
|  | ||||
|     return handled | ||||
|  | ||||
|  | ||||
| def show_logs(config: ConfigType, args, devices: list[str]) -> bool: | ||||
|     address = devices[0] | ||||
|     from .ble_logger import is_mac_address, logger_connect, logger_scan | ||||
|  | ||||
|     if devices[0] == "BLE": | ||||
|         ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) | ||||
|         if ble_device: | ||||
|             address = ble_device.address | ||||
|         else: | ||||
|             return True | ||||
|     if is_mac_address(address): | ||||
|         asyncio.run(logger_connect(address)) | ||||
|         return True | ||||
|     return False | ||||
|   | ||||
							
								
								
									
										60
									
								
								esphome/components/nrf52/ble_logger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								esphome/components/nrf52/ble_logger.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import re | ||||
| from typing import Final | ||||
|  | ||||
| from bleak import BleakClient, BleakScanner, BLEDevice | ||||
| from bleak.exc import ( | ||||
|     BleakCharacteristicNotFoundError, | ||||
|     BleakDBusError, | ||||
|     BleakDeviceNotFoundError, | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" | ||||
| NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" | ||||
|  | ||||
| MAC_ADDRESS_PATTERN: Final = re.compile( | ||||
|     r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE | ||||
| ) | ||||
|  | ||||
|  | ||||
| def is_mac_address(value: str) -> bool: | ||||
|     return MAC_ADDRESS_PATTERN.match(value) | ||||
|  | ||||
|  | ||||
| async def logger_scan(name: str) -> BLEDevice | None: | ||||
|     _LOGGER.info("Scanning bluetooth for %s...", name) | ||||
|     device = await BleakScanner.find_device_by_name(name) | ||||
|     if not device: | ||||
|         _LOGGER.error("%s Bluetooth LE device was not found!", name) | ||||
|     return device | ||||
|  | ||||
|  | ||||
| async def logger_connect(host: str) -> int | None: | ||||
|     disconnected_event = asyncio.Event() | ||||
|  | ||||
|     def handle_disconnect(client): | ||||
|         disconnected_event.set() | ||||
|  | ||||
|     def handle_rx(_, data: bytearray): | ||||
|         print(data.decode("utf-8"), end="") | ||||
|  | ||||
|     _LOGGER.info("Connecting %s...", host) | ||||
|     try: | ||||
|         async with BleakClient(host, disconnected_callback=handle_disconnect) as client: | ||||
|             _LOGGER.info("Connected %s...", host) | ||||
|             try: | ||||
|                 await client.start_notify(NUS_TX_CHAR_UUID, handle_rx) | ||||
|             except BleakDBusError as e: | ||||
|                 _LOGGER.error("Bluetooth LE logger: %s", e) | ||||
|                 disconnected_event.set() | ||||
|             await disconnected_event.wait() | ||||
|     except BleakDeviceNotFoundError: | ||||
|         _LOGGER.error("Device %s not found", host) | ||||
|         return 1 | ||||
|     except BleakCharacteristicNotFoundError: | ||||
|         _LOGGER.error("Device %s has no NUS characteristic", host) | ||||
|         return 1 | ||||
| @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(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)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -4,11 +4,14 @@ from esphome.components.esp32 import ( | ||||
|     VARIANT_ESP32H2, | ||||
|     add_idf_sdkconfig_option, | ||||
|     only_on_variant, | ||||
|     require_vfs_select, | ||||
| ) | ||||
| from esphome.components.mdns import MDNSComponent, enable_mdns_storage | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID | ||||
| from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS | ||||
| from esphome.core import CORE | ||||
| import esphome.final_validate as fv | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .const import ( | ||||
|     CONF_DEVICE_TYPE, | ||||
| @@ -106,6 +109,20 @@ _CONNECTION_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _validate(config: ConfigType) -> ConfigType: | ||||
|     if CONF_USE_ADDRESS not in config: | ||||
|         config[CONF_USE_ADDRESS] = f"{CORE.name}.local" | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def _require_vfs_select(config): | ||||
|     """Register VFS select requirement during config validation.""" | ||||
|     # OpenThread uses esp_vfs_eventfd which requires VFS select support | ||||
|     require_vfs_select() | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -117,11 +134,14 @@ CONFIG_SCHEMA = cv.All( | ||||
|             ), | ||||
|             cv.Optional(CONF_FORCE_DATASET): cv.boolean, | ||||
|             cv.Optional(CONF_TLV): cv.string_strict, | ||||
|             cv.Optional(CONF_USE_ADDRESS): cv.string_strict, | ||||
|         } | ||||
|     ).extend(_CONNECTION_SCHEMA), | ||||
|     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), | ||||
|     cv.only_with_esp_idf, | ||||
|     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), | ||||
|     _validate, | ||||
|     _require_vfs_select, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -145,6 +165,7 @@ async def to_code(config): | ||||
|     enable_mdns_storage() | ||||
|  | ||||
|     ot = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) | ||||
|     await cg.register_component(ot, config) | ||||
|  | ||||
|     srp = cg.new_Pvariable(config[CONF_SRP_ID]) | ||||
|   | ||||
| @@ -252,6 +252,12 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) { | ||||
|   ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); | ||||
| } | ||||
|  | ||||
| // set_use_address() is guaranteed to be called during component setup by Python code generation, | ||||
| // so use_address_ will always be valid when get_use_address() is called - no fallback needed. | ||||
| const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } | ||||
|  | ||||
| void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } | ||||
|  | ||||
| }  // namespace openthread | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -33,11 +33,15 @@ class OpenThreadComponent : public Component { | ||||
|   void on_factory_reset(std::function<void()> callback); | ||||
|   void defer_factory_reset_external_callback(); | ||||
|  | ||||
|   const std::string &get_use_address() const; | ||||
|   void set_use_address(const std::string &use_address); | ||||
|  | ||||
|  protected: | ||||
|   std::optional<otIp6Address> get_omr_address_(InstanceLock &lock); | ||||
|   bool teardown_started_{false}; | ||||
|   bool teardown_complete_{false}; | ||||
|   std::function<void()> factory_reset_external_callback_; | ||||
|   std::string use_address_; | ||||
| }; | ||||
|  | ||||
| extern OpenThreadComponent *global_openthread_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -38,7 +38,6 @@ void Pipsolar::loop() { | ||||
|   } | ||||
|   if (this->state_ == STATE_COMMAND_COMPLETE) { | ||||
|     if (this->check_incoming_length_(4)) { | ||||
|       ESP_LOGD(TAG, "response length for command OK"); | ||||
|       if (this->check_incoming_crc_()) { | ||||
|         // crc ok | ||||
|         if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { | ||||
| @@ -49,15 +48,15 @@ void Pipsolar::loop() { | ||||
|         this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|         this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|         this->state_ = STATE_IDLE; | ||||
|  | ||||
|       } else { | ||||
|         // crc failed | ||||
|         // no log message necessary, check_incoming_crc_() logs | ||||
|         this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|         this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|         this->state_ = STATE_IDLE; | ||||
|       } | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", | ||||
|       ESP_LOGD(TAG, "command %s response length not OK: with length %zu", | ||||
|                this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); | ||||
|       this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|       this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
| @@ -66,46 +65,10 @@ void Pipsolar::loop() { | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == STATE_POLL_CHECKED) { | ||||
|     switch (this->enabled_polling_commands_[this->last_polling_command_].identifier) { | ||||
|       case POLLING_QPIRI: | ||||
|         ESP_LOGD(TAG, "Decode QPIRI"); | ||||
|         handle_qpiri_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QPIGS: | ||||
|         ESP_LOGD(TAG, "Decode QPIGS"); | ||||
|         handle_qpigs_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QMOD: | ||||
|         ESP_LOGD(TAG, "Decode QMOD"); | ||||
|         handle_qmod_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QFLAG: | ||||
|         ESP_LOGD(TAG, "Decode QFLAG"); | ||||
|         handle_qflag_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QPIWS: | ||||
|         ESP_LOGD(TAG, "Decode QPIWS"); | ||||
|         handle_qpiws_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QT: | ||||
|         ESP_LOGD(TAG, "Decode QT"); | ||||
|         handle_qt_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QMN: | ||||
|         ESP_LOGD(TAG, "Decode QMN"); | ||||
|         handle_qmn_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       default: | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|     } | ||||
|     ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|     this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier, | ||||
|                                 (const char *) this->read_buffer_); | ||||
|     this->state_ = STATE_IDLE; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| @@ -113,6 +76,8 @@ void Pipsolar::loop() { | ||||
|     if (this->check_incoming_crc_()) { | ||||
|       if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && | ||||
|           this->read_buffer_[3] == 'K') { | ||||
|         ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|         this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         return; | ||||
|       } | ||||
| @@ -121,6 +86,9 @@ void Pipsolar::loop() { | ||||
|       this->state_ = STATE_POLL_CHECKED; | ||||
|       return; | ||||
|     } else { | ||||
|       // crc failed | ||||
|       // no log message necessary, check_incoming_crc_() logs | ||||
|       this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|       this->state_ = STATE_IDLE; | ||||
|     } | ||||
|   } | ||||
| @@ -158,21 +126,19 @@ void Pipsolar::loop() { | ||||
|       // command timeout | ||||
|       const char *command = this->command_queue_[this->command_queue_position_].c_str(); | ||||
|       this->command_start_millis_ = millis(); | ||||
|       ESP_LOGD(TAG, "timeout command from queue: %s", command); | ||||
|       ESP_LOGD(TAG, "command %s timeout", command); | ||||
|       this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|       this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|       this->state_ = STATE_IDLE; | ||||
|       return; | ||||
|     } else { | ||||
|     } | ||||
|   } | ||||
|   if (this->state_ == STATE_POLL) { | ||||
|     if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { | ||||
|       // command timeout | ||||
|       ESP_LOGD(TAG, "timeout command to poll: %s", | ||||
|                this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|       ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|       this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|       this->state_ = STATE_IDLE; | ||||
|     } else { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -187,7 +153,6 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { | ||||
| uint8_t Pipsolar::check_incoming_crc_() { | ||||
|   uint16_t crc16; | ||||
|   crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); | ||||
|   ESP_LOGD(TAG, "checking crc on incoming message"); | ||||
|   if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && | ||||
|       ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { | ||||
|     ESP_LOGD(TAG, "CRC OK"); | ||||
| @@ -253,7 +218,7 @@ bool Pipsolar::send_next_poll_() { | ||||
|     this->write(((uint8_t) ((crc16) &0xff)));  // lowbyte | ||||
|     // end Byte | ||||
|     this->write(0x0D); | ||||
|     ESP_LOGD(TAG, "Sending polling command : %s with length %d", | ||||
|     ESP_LOGD(TAG, "Sending polling command: %s with length %d", | ||||
|              this->enabled_polling_commands_[this->last_polling_command_].command, | ||||
|              this->enabled_polling_commands_[this->last_polling_command_].length); | ||||
|     return true; | ||||
| @@ -274,6 +239,38 @@ void Pipsolar::queue_command(const std::string &command) { | ||||
|   ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str()); | ||||
| } | ||||
|  | ||||
| void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) { | ||||
|   switch (polling_command) { | ||||
|     case POLLING_QPIRI: | ||||
|       handle_qpiri_(message); | ||||
|       break; | ||||
|     case POLLING_QPIGS: | ||||
|       handle_qpigs_(message); | ||||
|       break; | ||||
|     case POLLING_QMOD: | ||||
|       handle_qmod_(message); | ||||
|       break; | ||||
|     case POLLING_QFLAG: | ||||
|       handle_qflag_(message); | ||||
|       break; | ||||
|     case POLLING_QPIWS: | ||||
|       handle_qpiws_(message); | ||||
|       break; | ||||
|     case POLLING_QT: | ||||
|       handle_qt_(message); | ||||
|       break; | ||||
|     case POLLING_QMN: | ||||
|       handle_qmn_(message); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) { | ||||
|   // handlers are designed in a way that an empty message sets all sensors to unknown | ||||
|   this->handle_poll_response_(polling_command, ""); | ||||
| } | ||||
|  | ||||
| void Pipsolar::handle_qpiri_(const char *message) { | ||||
|   if (this->last_qpiri_) { | ||||
|     this->last_qpiri_->publish_state(message); | ||||
|   | ||||
| @@ -204,6 +204,9 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { | ||||
|   bool send_next_command_(); | ||||
|   bool send_next_poll_(); | ||||
|  | ||||
|   void handle_poll_response_(ENUMPollingCommand polling_command, const char *message); | ||||
|   void handle_poll_error_(ENUMPollingCommand polling_command); | ||||
|   // these handlers are designed in a way that an empty message sets all sensors to unknown | ||||
|   void handle_qpiri_(const char *message); | ||||
|   void handle_qpigs_(const char *message); | ||||
|   void handle_qmod_(const char *message); | ||||
|   | ||||
| @@ -4,11 +4,18 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BATTERY_VOLTAGE, | ||||
|     CONF_BUS_VOLTAGE, | ||||
|     DEVICE_CLASS_APPARENT_POWER, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_FREQUENCY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_BATTERY, | ||||
|     ICON_CURRENT_AC, | ||||
|     ICON_FLASH, | ||||
|     ICON_GAUGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HERTZ, | ||||
| @@ -22,6 +29,10 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
| ICON_SOLAR_POWER = "mdi:solar-power" | ||||
| ICON_SOLAR_PANEL = "mdi:solar-panel" | ||||
| ICON_CURRENT_DC = "mdi:current-dc" | ||||
|  | ||||
| # QPIRI sensors | ||||
| CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" | ||||
| CONF_GRID_RATING_CURRENT = "grid_rating_current" | ||||
| @@ -75,16 +86,19 @@ TYPES = { | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_GRID_RATING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
| @@ -98,11 +112,12 @@ TYPES = { | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT_AMPS, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_APPARENT_POWER, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|     ), | ||||
|     CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( | ||||
| @@ -131,124 +146,151 @@ TYPES = { | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|     ), | ||||
|     CONF_BATTERY_TYPE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|     ), | ||||
|     CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|     ), | ||||
|     CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_MACHINE_TYPE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_TOPOLOGY: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_OUTPUT_MODE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_PV_POWER_BALANCE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_GRID_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_GRID_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
|         icon=ICON_CURRENT_AC, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_FREQUENCY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
|         icon=ICON_CURRENT_AC, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_FREQUENCY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT_AMPS, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_APPARENT_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_PERCENT, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_GAUGE, | ||||
|         accuracy_decimals=0, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BUS_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_FLASH, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_BATTERY, | ||||
|         accuracy_decimals=2, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_CURRENT_DC, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_PERCENT, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_BATTERY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_CELSIUS, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         icon=ICON_SOLAR_PANEL, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         icon=ICON_SOLAR_PANEL, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=2, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_CURRENT_DC, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
| @@ -256,12 +298,14 @@ TYPES = { | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|     ), | ||||
|     CONF_EEPROM_VERSION: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|         accuracy_decimals=0, | ||||
|     ), | ||||
|     CONF_PV_CHARGING_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         accuracy_decimals=1, | ||||
|         icon=ICON_SOLAR_POWER, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,25 @@ | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| // IDF version 5.5.1 and above is required because of a bug in | ||||
| // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 | ||||
| typedef union {  // NOLINT(modernize-use-using) | ||||
|   struct { | ||||
|     uint16_t duration : 15; | ||||
|     uint16_t level : 1; | ||||
|   }; | ||||
|   uint16_t val; | ||||
| } rmt_symbol_half_t; | ||||
|  | ||||
| struct RemoteTransmitterComponentStore { | ||||
|   uint32_t times{0}; | ||||
|   uint32_t index{0}; | ||||
| }; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|                                    public Component | ||||
| #ifdef USE_ESP32 | ||||
| @@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
|   std::vector<rmt_symbol_half_t> rmt_temp_; | ||||
| #else | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
| #endif | ||||
|   uint32_t current_carrier_frequency_{38000}; | ||||
|   bool initialized_{false}; | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
|   bool with_dma_{false}; | ||||
|   bool eot_level_{false}; | ||||
|   rmt_channel_handle_t channel_{NULL}; | ||||
|   | ||||
| @@ -10,6 +10,46 @@ namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| // Maximum RMT symbol duration (15-bit field) | ||||
| static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, | ||||
|                                              rmt_symbol_word_t *symbols, bool *done, void *arg) { | ||||
|   auto *store = static_cast<RemoteTransmitterComponentStore *>(arg); | ||||
|   const auto *encoded = static_cast<const rmt_symbol_half_t *>(data); | ||||
|   size_t length = size / sizeof(rmt_symbol_half_t); | ||||
|   size_t count = 0; | ||||
|  | ||||
|   // copy symbols | ||||
|   for (size_t i = 0; i < free; i++) { | ||||
|     uint16_t sym_0 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0; | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     uint16_t sym_1 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|   } | ||||
|   *done = false; | ||||
|   return count; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->inverted_ = this->pin_->is_inverted(); | ||||
|   this->configure_rmt_(); | ||||
| @@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   rmt_symbol_half_t symbol = { | ||||
|       .duration = 1, | ||||
|       .level = value, | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = value; | ||||
|   this->store_.times = 1; | ||||
|   this->store_.index = 0; | ||||
| #else | ||||
|   rmt_symbol_word_t symbol = { | ||||
|       .duration0 = 1, | ||||
|       .level0 = value, | ||||
| @@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.loop_count = 0; | ||||
|   config.flags.eot_level = value; | ||||
| #endif | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
| @@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); | ||||
|     } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|     rmt_simple_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     encoder.callback = encoder_callback; | ||||
|     encoder.arg = &this->store_; | ||||
|     encoder.min_chunk_size = 1; | ||||
|     error = rmt_new_simple_encoder(&encoder, &this->encoder_); | ||||
|     if (error != ESP_OK) { | ||||
|       this->error_code_ = error; | ||||
|       this->error_string_ = "in rmt_new_simple_encoder"; | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #else | ||||
|     rmt_copy_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     error = rmt_new_copy_encoder(&encoder, &this->encoder_); | ||||
| @@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     error = rmt_enable(this->channel_); | ||||
|     if (error != ESP_OK) { | ||||
| @@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
|   } | ||||
|  | ||||
|   this->rmt_temp_.clear(); | ||||
|   this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|     this->rmt_temp_.push_back({ | ||||
|         .duration = static_cast<uint16_t>(duration), | ||||
|         .level = static_cast<uint16_t>(this->eot_level_), | ||||
|     }); | ||||
|     send_wait -= duration; | ||||
|   } | ||||
|  | ||||
|   // encode data | ||||
|   size_t offset = this->rmt_temp_.size(); | ||||
|   for (int32_t value : this->temp_.get_data()) { | ||||
|     bool level = value >= 0; | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       this->rmt_temp_.push_back({ | ||||
|           .duration = static_cast<uint16_t>(duration), | ||||
|           .level = static_cast<uint16_t>(level ^ this->inverted_), | ||||
|       }); | ||||
|       value -= duration; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { | ||||
|     ESP_LOGE(TAG, "Empty data"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->transmit_trigger_->trigger(); | ||||
|  | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = this->eot_level_; | ||||
|   this->store_.times = send_times; | ||||
|   this->store_.index = offset; | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                  this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) | ||||
|     return; | ||||
| @@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     val = this->from_microseconds_(static_cast<uint32_t>(val)); | ||||
|  | ||||
|     do { | ||||
|       int32_t item = std::min(val, int32_t(32767)); | ||||
|       int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       val -= item; | ||||
|  | ||||
|       if (rmt_i % 2 == 0) { | ||||
| @@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     rmt_transmit_config_t config; | ||||
|     memset(&config, 0, sizeof(config)); | ||||
|     config.loop_count = 0; | ||||
|     config.flags.eot_level = this->eot_level_; | ||||
|     esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                    this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); | ||||
| @@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace rp2040 | ||||
|   | ||||
| @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(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)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -28,6 +28,8 @@ from esphome.const import ( | ||||
|     CONF_ON_RAW_VALUE, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_ON_VALUE_RANGE, | ||||
|     CONF_OPTIMISTIC, | ||||
|     CONF_PERIOD, | ||||
|     CONF_QUANTILE, | ||||
|     CONF_SEND_EVERY, | ||||
|     CONF_SEND_FIRST_AT, | ||||
| @@ -644,10 +646,29 @@ async def throttle_with_priority_filter_to_code(config, filter_id): | ||||
|     return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) | ||||
|  | ||||
|  | ||||
| HEARTBEAT_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @FILTER_REGISTRY.register( | ||||
|     "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds | ||||
|     "heartbeat", | ||||
|     HeartbeatFilter, | ||||
|     cv.Any( | ||||
|         cv.positive_time_period_milliseconds, | ||||
|         HEARTBEAT_SCHEMA, | ||||
|     ), | ||||
| ) | ||||
| async def heartbeat_filter_to_code(config, filter_id): | ||||
|     if isinstance(config, dict): | ||||
|         var = cg.new_Pvariable(filter_id, config[CONF_PERIOD]) | ||||
|         await cg.register_component(var, {}) | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         return var | ||||
|  | ||||
|     var = cg.new_Pvariable(filter_id, config) | ||||
|     await cg.register_component(var, {}) | ||||
|     return var | ||||
| @@ -857,7 +878,9 @@ async def setup_sensor_core_(var, config): | ||||
|         cg.add(var.set_unit_of_measurement(unit_of_measurement)) | ||||
|     if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: | ||||
|         cg.add(var.set_accuracy_decimals(accuracy_decimals)) | ||||
|     cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) | ||||
|     # Only set force_update if True (default is False) | ||||
|     if config[CONF_FORCE_UPDATE]: | ||||
|         cg.add(var.set_force_update(True)) | ||||
|     if config.get(CONF_FILTERS):  # must exist and not be empty | ||||
|         filters = await build_filters(config[CONF_FILTERS]) | ||||
|         cg.add(var.set_filters(filters)) | ||||
|   | ||||
| @@ -313,7 +313,7 @@ optional<float> DeltaFilter::new_value(float value) { | ||||
| } | ||||
|  | ||||
| // OrFilter | ||||
| OrFilter::OrFilter(std::vector<Filter *> filters) : filters_(std::move(filters)), phi_(this) {} | ||||
| OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {} | ||||
| OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} | ||||
|  | ||||
| optional<float> OrFilter::PhiNode::new_value(float value) { | ||||
| @@ -326,14 +326,14 @@ optional<float> OrFilter::PhiNode::new_value(float value) { | ||||
| } | ||||
| optional<float> OrFilter::new_value(float value) { | ||||
|   this->has_value_ = false; | ||||
|   for (Filter *filter : this->filters_) | ||||
|   for (auto *filter : this->filters_) | ||||
|     filter->input(value); | ||||
|  | ||||
|   return {}; | ||||
| } | ||||
| void OrFilter::initialize(Sensor *parent, Filter *next) { | ||||
|   Filter::initialize(parent, next); | ||||
|   for (Filter *filter : this->filters_) { | ||||
|   for (auto *filter : this->filters_) { | ||||
|     filter->initialize(parent, &this->phi_); | ||||
|   } | ||||
|   this->phi_.initialize(parent, nullptr); | ||||
| @@ -372,8 +372,12 @@ optional<float> HeartbeatFilter::new_value(float value) { | ||||
|   this->last_input_ = value; | ||||
|   this->has_value_ = true; | ||||
|  | ||||
|   if (this->optimistic_) { | ||||
|     return value; | ||||
|   } | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
| void HeartbeatFilter::setup() { | ||||
|   this->set_interval("heartbeat", this->time_period_, [this]() { | ||||
|     ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), | ||||
| @@ -384,20 +388,27 @@ void HeartbeatFilter::setup() { | ||||
|     this->output(this->last_input_); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions) | ||||
|     : linear_functions_(linear_functions) {} | ||||
|  | ||||
| optional<float> CalibrateLinearFilter::new_value(float value) { | ||||
|   for (std::array<float, 3> f : this->linear_functions_) { | ||||
|   for (const auto &f : this->linear_functions_) { | ||||
|     if (!std::isfinite(f[2]) || value < f[2]) | ||||
|       return (value * f[0]) + f[1]; | ||||
|   } | ||||
|   return NAN; | ||||
| } | ||||
|  | ||||
| CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients) | ||||
|     : coefficients_(coefficients) {} | ||||
|  | ||||
| optional<float> CalibratePolynomialFilter::new_value(float value) { | ||||
|   float res = 0.0f; | ||||
|   float x = 1.0f; | ||||
|   for (float coefficient : this->coefficients_) { | ||||
|   for (const auto &coefficient : this->coefficients_) { | ||||
|     res += x * coefficient; | ||||
|     x *= value; | ||||
|   } | ||||
|   | ||||
| @@ -396,15 +396,16 @@ class HeartbeatFilter : public Filter, public Component { | ||||
|   explicit HeartbeatFilter(uint32_t time_period); | ||||
|  | ||||
|   void setup() override; | ||||
|  | ||||
|   optional<float> new_value(float value) override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|  | ||||
|  protected: | ||||
|   uint32_t time_period_; | ||||
|   float last_input_; | ||||
|   bool has_value_{false}; | ||||
|   bool optimistic_{false}; | ||||
| }; | ||||
|  | ||||
| class DeltaFilter : public Filter { | ||||
| @@ -422,7 +423,7 @@ class DeltaFilter : public Filter { | ||||
|  | ||||
| class OrFilter : public Filter { | ||||
|  public: | ||||
|   explicit OrFilter(std::vector<Filter *> filters); | ||||
|   explicit OrFilter(std::initializer_list<Filter *> filters); | ||||
|  | ||||
|   void initialize(Sensor *parent, Filter *next) override; | ||||
|  | ||||
| @@ -438,28 +439,27 @@ class OrFilter : public Filter { | ||||
|     OrFilter *or_parent_; | ||||
|   }; | ||||
|  | ||||
|   std::vector<Filter *> filters_; | ||||
|   FixedVector<Filter *> filters_; | ||||
|   PhiNode phi_; | ||||
|   bool has_value_{false}; | ||||
| }; | ||||
|  | ||||
| class CalibrateLinearFilter : public Filter { | ||||
|  public: | ||||
|   CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions) | ||||
|       : linear_functions_(std::move(linear_functions)) {} | ||||
|   explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions); | ||||
|   optional<float> new_value(float value) override; | ||||
|  | ||||
|  protected: | ||||
|   std::vector<std::array<float, 3>> linear_functions_; | ||||
|   FixedVector<std::array<float, 3>> linear_functions_; | ||||
| }; | ||||
|  | ||||
| class CalibratePolynomialFilter : public Filter { | ||||
|  public: | ||||
|   CalibratePolynomialFilter(std::vector<float> coefficients) : coefficients_(std::move(coefficients)) {} | ||||
|   explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients); | ||||
|   optional<float> new_value(float value) override; | ||||
|  | ||||
|  protected: | ||||
|   std::vector<float> coefficients_; | ||||
|   FixedVector<float> coefficients_; | ||||
| }; | ||||
|  | ||||
| class ClampFilter : public Filter { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ void SNTPComponent::setup() { | ||||
|   esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     esp_sntp_setservername(i++, server.c_str()); | ||||
|     esp_sntp_setservername(i++, server); | ||||
|   } | ||||
|   esp_sntp_set_sync_interval(this->get_update_interval()); | ||||
|   esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { | ||||
| @@ -42,7 +42,7 @@ void SNTPComponent::setup() { | ||||
|  | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     sntp_setservername(i++, server.c_str()); | ||||
|     sntp_setservername(i++, server); | ||||
|   } | ||||
|  | ||||
| #if defined(USE_ESP8266) | ||||
| @@ -59,7 +59,7 @@ void SNTPComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "SNTP Time:"); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server.c_str()); | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server); | ||||
|   } | ||||
| } | ||||
| void SNTPComponent::update() { | ||||
|   | ||||
| @@ -2,10 +2,14 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #include <array> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace sntp { | ||||
|  | ||||
| // Server count is calculated at compile time by Python codegen | ||||
| // SNTP_SERVER_COUNT will always be defined | ||||
|  | ||||
| /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. | ||||
| /// | ||||
| /// \note | ||||
| @@ -14,10 +18,7 @@ namespace sntp { | ||||
| /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | ||||
| class SNTPComponent : public time::RealTimeClock { | ||||
|  public: | ||||
|   SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {} | ||||
|  | ||||
|   // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would | ||||
|   // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. | ||||
|   SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {} | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -29,7 +30,10 @@ class SNTPComponent : public time::RealTimeClock { | ||||
|   void time_synced(); | ||||
|  | ||||
|  protected: | ||||
|   std::vector<std::string> servers_; | ||||
|   // Store const char pointers to string literals | ||||
|   // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) | ||||
|   // Other platforms: strings in flash | ||||
|   std::array<const char *, SNTP_SERVER_COUNT> servers_; | ||||
|   bool has_time_{false}; | ||||
|  | ||||
| #if defined(USE_ESP32) | ||||
|   | ||||
| @@ -43,6 +43,11 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
| async def to_code(config): | ||||
|     servers = config[CONF_SERVERS] | ||||
|  | ||||
|     # Define server count at compile time | ||||
|     cg.add_define("SNTP_SERVER_COUNT", len(servers)) | ||||
|  | ||||
|     # Pass string literals to constructor - stored in flash/rodata by compiler | ||||
|     var = cg.new_Pvariable(config[CONF_ID], servers) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -10,6 +10,9 @@ from .. import template_ns | ||||
| TemplateBinarySensor = template_ns.class_( | ||||
|     "TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
| StatelessTemplateBinarySensor = template_ns.class_( | ||||
|     "StatelessTemplateBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     binary_sensor.binary_sensor_schema(TemplateBinarySensor) | ||||
| @@ -26,15 +29,22 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     # Check if we have a lambda first - determines which class to instantiate | ||||
|     if lamb := config.get(CONF_LAMBDA): | ||||
|         # Use new_lambda_pvariable to create either TemplateBinarySensor or StatelessTemplateBinarySensor | ||||
|         template_ = await cg.process_lambda( | ||||
|             lamb, [], return_type=cg.optional.template(bool) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|     if condition := config.get(CONF_CONDITION): | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateBinarySensor | ||||
|         ) | ||||
|         # Manually register as binary sensor since we didn't use new_binary_sensor | ||||
|         await binary_sensor.register_binary_sensor(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     elif condition := config.get(CONF_CONDITION): | ||||
|         # For conditions, create stateful version and set template | ||||
|         var = await binary_sensor.new_binary_sensor(config) | ||||
|         await cg.register_component(var, config) | ||||
|         condition = await automation.build_condition( | ||||
|             condition, cg.TemplateArguments(), [] | ||||
|         ) | ||||
| @@ -42,6 +52,10 @@ async def to_code(config): | ||||
|             f"return {condition.check()};", [], return_type=cg.optional.template(bool) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|     else: | ||||
|         # No lambda or condition - just create the base template sensor | ||||
|         var = await binary_sensor.new_binary_sensor(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|   | ||||
| @@ -6,18 +6,13 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.binary_sensor"; | ||||
|  | ||||
| void TemplateBinarySensor::setup() { this->loop(); } | ||||
|  | ||||
| void TemplateBinarySensor::loop() { | ||||
|   if (this->f_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto s = this->f_(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateBinarySensorBase<F>::dump_config() { | ||||
|   LOG_BINARY_SENSOR("", "Template Binary Sensor", this); | ||||
| } | ||||
| void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } | ||||
|  | ||||
| template class TemplateBinarySensorBase<std::function<optional<bool>()>>; | ||||
| template class TemplateBinarySensorBase<optional<bool> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -6,18 +6,41 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
| template<typename F> class TemplateBinarySensorBase : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
|   void setup() override { this->loop(); } | ||||
|  | ||||
|   void loop() override { | ||||
|     if (this->f_ == nullptr) | ||||
|       return; | ||||
|     auto s = this->f_(); | ||||
|     if (s.has_value()) { | ||||
|       this->publish_state(*s); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   std::function<optional<bool>()> f_{nullptr}; | ||||
|   F f_; | ||||
| }; | ||||
|  | ||||
| class TemplateBinarySensor : public TemplateBinarySensorBase<std::function<optional<bool>()>> { | ||||
|  public: | ||||
|   TemplateBinarySensor() { this->f_ = nullptr; } | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template binary sensor 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 StatelessTemplateBinarySensor : public TemplateBinarySensorBase<optional<bool> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateBinarySensor(optional<bool> (*f)()) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -23,6 +23,9 @@ from esphome.const import ( | ||||
| from .. import template_ns | ||||
|  | ||||
| TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component) | ||||
| StatelessTemplateCover = template_ns.class_( | ||||
|     "StatelessTemplateCover", cover.Cover, cg.Component | ||||
| ) | ||||
|  | ||||
| TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode") | ||||
| RESTORE_MODES = { | ||||
| @@ -63,13 +66,22 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await cover.new_cover(config) | ||||
|     await cg.register_component(var, config) | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either TemplateCover or StatelessTemplateCover | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(float) | ||||
|         ) | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateCover | ||||
|         ) | ||||
|         # Manually register as cover since we didn't use new_cover | ||||
|         await cover.register_cover(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     else: | ||||
|         # No state lambda - just create the base template cover | ||||
|         var = await cover.new_cover(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|     if CONF_OPEN_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_open_trigger(), [], config[CONF_OPEN_ACTION] | ||||
|   | ||||
| @@ -8,14 +8,8 @@ using namespace esphome::cover; | ||||
|  | ||||
| static const char *const TAG = "template.cover"; | ||||
|  | ||||
| TemplateCover::TemplateCover() | ||||
|     : open_trigger_(new Trigger<>()), | ||||
|       close_trigger_(new Trigger<>), | ||||
|       stop_trigger_(new Trigger<>()), | ||||
|       toggle_trigger_(new Trigger<>()), | ||||
|       position_trigger_(new Trigger<float>()), | ||||
|       tilt_trigger_(new Trigger<float>()) {} | ||||
| void TemplateCover::setup() { | ||||
| // Template instantiations | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::setup() { | ||||
|   switch (this->restore_mode_) { | ||||
|     case COVER_NO_RESTORE: | ||||
|       break; | ||||
| @@ -34,43 +28,12 @@ void TemplateCover::setup() { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void TemplateCover::loop() { | ||||
|   bool changed = false; | ||||
|  | ||||
|   if (this->state_f_.has_value()) { | ||||
|     auto s = (*this->state_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|       if (pos != this->position) { | ||||
|         this->position = pos; | ||||
|         changed = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (this->tilt_f_.has_value()) { | ||||
|     auto s = (*this->tilt_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto tilt = clamp(*s, 0.0f, 1.0f); | ||||
|       if (tilt != this->tilt) { | ||||
|         this->tilt = tilt; | ||||
|         changed = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (changed) | ||||
|     this->publish_state(); | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::dump_config() { | ||||
|   LOG_COVER("", "Template Cover", this); | ||||
| } | ||||
| void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateCover::set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
| float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } | ||||
| Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } | ||||
| Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } | ||||
| Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } | ||||
| void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } | ||||
| void TemplateCover::control(const CoverCall &call) { | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::control(const CoverCall &call) { | ||||
|   if (call.get_stop()) { | ||||
|     this->stop_prev_trigger_(); | ||||
|     this->stop_trigger_->trigger(); | ||||
| @@ -113,7 +76,8 @@ void TemplateCover::control(const CoverCall &call) { | ||||
|  | ||||
|   this->publish_state(); | ||||
| } | ||||
| CoverTraits TemplateCover::get_traits() { | ||||
|  | ||||
| template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF, TiltF>::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   traits.set_supports_stop(this->has_stop_); | ||||
| @@ -122,19 +86,16 @@ CoverTraits TemplateCover::get_traits() { | ||||
|   traits.set_supports_tilt(this->has_tilt_); | ||||
|   return traits; | ||||
| } | ||||
| Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } | ||||
| Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
| void TemplateCover::set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } | ||||
| void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
| void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
| void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
| void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } | ||||
| void TemplateCover::stop_prev_trigger_() { | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::stop_prev_trigger_() { | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| template class TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>>; | ||||
| template class TemplateCoverBase<optional<float> (*)(), optional<float> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -13,31 +13,59 @@ enum TemplateCoverRestoreMode { | ||||
|   COVER_RESTORE_AND_CALL, | ||||
| }; | ||||
|  | ||||
| class TemplateCover : public cover::Cover, public Component { | ||||
| template<typename StateF, typename TiltF> class TemplateCoverBase : public cover::Cover, public Component { | ||||
|  public: | ||||
|   TemplateCover(); | ||||
|   TemplateCoverBase() | ||||
|       : open_trigger_(new Trigger<>()), | ||||
|         close_trigger_(new Trigger<>()), | ||||
|         stop_trigger_(new Trigger<>()), | ||||
|         toggle_trigger_(new Trigger<>()), | ||||
|         position_trigger_(new Trigger<float>()), | ||||
|         tilt_trigger_(new Trigger<float>()) {} | ||||
|  | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f); | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
|   Trigger<> *get_toggle_trigger() const; | ||||
|   Trigger<float> *get_position_trigger() const; | ||||
|   Trigger<float> *get_tilt_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
|   void set_assumed_state(bool assumed_state); | ||||
|   void set_tilt_lambda(std::function<optional<float>()> &&tilt_f); | ||||
|   void set_has_stop(bool has_stop); | ||||
|   void set_has_position(bool has_position); | ||||
|   void set_has_tilt(bool has_tilt); | ||||
|   void set_has_toggle(bool has_toggle); | ||||
|   void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } | ||||
|   void loop() override { | ||||
|     bool changed = false; | ||||
|     if (this->state_f_.has_value()) { | ||||
|       auto s = (*this->state_f_)(); | ||||
|       if (s.has_value()) { | ||||
|         auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|         if (pos != this->position) { | ||||
|           this->position = pos; | ||||
|           changed = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (this->tilt_f_.has_value()) { | ||||
|       auto s = (*this->tilt_f_)(); | ||||
|       if (s.has_value()) { | ||||
|         auto tilt = clamp(*s, 0.0f, 1.0f); | ||||
|         if (tilt != this->tilt) { | ||||
|           this->tilt = tilt; | ||||
|           changed = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (changed) | ||||
|       this->publish_state(); | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   Trigger<> *get_open_trigger() const { return this->open_trigger_; } | ||||
|   Trigger<> *get_close_trigger() const { return this->close_trigger_; } | ||||
|   Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } | ||||
|   Trigger<> *get_toggle_trigger() const { return this->toggle_trigger_; } | ||||
|   Trigger<float> *get_position_trigger() const { return this->position_trigger_; } | ||||
|   Trigger<float> *get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
|   void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
|   void set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
|   void set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } | ||||
|   void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
|   void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const cover::CoverCall &call) override; | ||||
| @@ -45,8 +73,8 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<std::function<optional<float>()>> tilt_f_; | ||||
|   optional<StateF> state_f_; | ||||
|   optional<TiltF> tilt_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
| @@ -62,5 +90,22 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   bool has_tilt_{false}; | ||||
| }; | ||||
|  | ||||
| class TemplateCover : public TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>> { | ||||
|  public: | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
|   void set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template cover for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateCover : public TemplateCoverBase<optional<float> (*)(), optional<float> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateCover(optional<float> (*state_f)()) { this->state_f_ = state_f; } | ||||
|   void set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DAY, | ||||
|     CONF_HOUR, | ||||
|     CONF_ID, | ||||
|     CONF_INITIAL_VALUE, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MINUTE, | ||||
| @@ -25,14 +26,23 @@ CODEOWNERS = ["@rfdarter"] | ||||
| TemplateDate = template_ns.class_( | ||||
|     "TemplateDate", datetime.DateEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateDate = template_ns.class_( | ||||
|     "StatelessTemplateDate", datetime.DateEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| TemplateTime = template_ns.class_( | ||||
|     "TemplateTime", datetime.TimeEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateTime = template_ns.class_( | ||||
|     "StatelessTemplateTime", datetime.TimeEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| TemplateDateTime = template_ns.class_( | ||||
|     "TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateDateTime = template_ns.class_( | ||||
|     "StatelessTemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate(config): | ||||
| @@ -99,15 +109,30 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await datetime.new_datetime(config) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either Template* or StatelessTemplate* | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.ESPTime) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|         # Determine the appropriate stateless class based on type | ||||
|         if config[CONF_TYPE] == "DATE": | ||||
|             stateless_class = StatelessTemplateDate | ||||
|         elif config[CONF_TYPE] == "TIME": | ||||
|             stateless_class = StatelessTemplateTime | ||||
|         else:  # DATETIME | ||||
|             stateless_class = StatelessTemplateDateTime | ||||
|  | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, stateless_class | ||||
|         ) | ||||
|         # Manually register as datetime since we didn't use new_datetime | ||||
|         await datetime.register_datetime(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     else: | ||||
|         # No lambda - just create the base template datetime | ||||
|         var = await datetime.new_datetime(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||
|  | ||||
| @@ -146,5 +171,3 @@ async def to_code(config): | ||||
|             [(cg.ESPTime, "x")], | ||||
|             config[CONF_SET_ACTION], | ||||
|         ) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -9,7 +9,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.date"; | ||||
|  | ||||
| void TemplateDate::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateDateBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -36,21 +37,7 @@ void TemplateDate::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TemplateDate::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->year_ = val->year; | ||||
|   this->month_ = val->month; | ||||
|   this->day_ = val->day_of_month; | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TemplateDate::control(const datetime::DateCall &call) { | ||||
| template<typename F> void TemplateDateBase<F>::control(const datetime::DateCall &call) { | ||||
|   bool has_year = call.get_year().has_value(); | ||||
|   bool has_month = call.get_month().has_value(); | ||||
|   bool has_day = call.get_day().has_value(); | ||||
| @@ -99,12 +86,15 @@ void TemplateDate::control(const datetime::DateCall &call) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateDate::dump_config() { | ||||
| template<typename F> void TemplateDateBase<F>::dump_config() { | ||||
|   LOG_DATETIME_DATE("", "Template Date", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| template class TemplateDateBase<std::function<optional<ESPTime>()>>; | ||||
| template class TemplateDateBase<optional<ESPTime> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // 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