Compare commits

..

70 Commits

Author SHA1 Message Date
J. Nick Koston
5426f8736b [esphome][ota] Add write_byte_() helper to reduce code duplication 2025-10-23 22:58:09 -07:00
J. Nick Koston
2c85ba037e [http_request] Pass collect_headers by const reference instead of by value (#11494) 2025-10-23 20:01:48 -07:00
J. Nick Koston
2440bbdceb [core][sensor] Eliminate redundant default value setters in generated code (#11495) 2025-10-23 20:01:23 -07:00
Jesse Hills
3ac8eb7696 Merge branch 'release' into dev 2025-10-24 14:08:56 +13:00
Jesse Hills
6a478b9070 Merge pull request #11506 from esphome/bump-2025.10.3
2025.10.3
2025-10-24 14:08:12 +13:00
Jesse Hills
a32a1d11fb Bump version to 2025.10.3 2025-10-24 07:51:38 +13:00
Markus
daeb8ef88c [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-24 07:51:38 +13:00
Anton Sergunov
febee437d6 [uart] Make rx pin respect pullup and pulldown settings (#9248) 2025-10-24 07:51:38 +13:00
Peter Zich
de2f475dbd [hdc1080] Make HDC1080_CMD_CONFIGURATION failure a warning (and log it) (#11355)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-24 07:51:38 +13:00
Markus
fa3ec6f732 [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-23 11:32:07 -07:00
dependabot[bot]
e490aec6b4 Bump ruamel-yaml from 0.18.15 to 0.18.16 (#11482)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 10:25:36 -07:00
J. Nick Koston
8da8095a6a [tests] Isolate gps component to prevent TinyGPSPlus millis() conflicts (#11499) 2025-10-23 10:11:13 -07:00
Patrick
ab14c0cd72 [pipsolar] improve sensor readout in HA, set unknown state on timeout / error (#10292)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-23 11:32:02 -04:00
J. Nick Koston
917deac7cb [scheduler] Remove unused <deque> include after defer queue optimization (#11491) 2025-10-23 04:02:19 +00:00
dependabot[bot]
3d21adecd3 Bump aioesphomeapi from 42.2.0 to 42.3.0 (#11493)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 02:58:09 +00:00
J. Nick Koston
5b023f9369 [ethernet] Add RMII GPIO pin conflict validation (#11488) 2025-10-22 16:37:50 -10:00
dependabot[bot]
6c2ce5cacf Bump bleak from 1.0.1 to 1.1.1 (#11492)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-22 16:36:30 -10:00
J. Nick Koston
d23e25f099 [api] Fix clang-tidy modernize-use-emplace warning for light effects (#11490) 2025-10-22 21:31:51 -05:00
optimusprimespace
9b78098eec [hdc2010] New component (#6674)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-22 22:24:17 -04:00
Keith Burzinski
7e5b82c5f3 [improv_serial] Various optimizations (#11473)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-23 14:24:08 +13:00
J. Nick Koston
2864e989bd [light] Extract ColorModeMask into generic FiniteSetMask helper (#11472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-23 14:22:46 +13:00
J. Nick Koston
6efe346cc5 [light] Use std::initializer_list for add_effects to reduce flash overhead (#11485) 2025-10-23 14:21:53 +13:00
J. Nick Koston
f2f6c597ef [light] Store effect names in flash (const char*) to save RAM (#11487)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 14:17:57 +13:00
tomaszduda23
b91b12d77a [nrf52] support BLE --device for logging (#9861)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-22 14:55:34 -10:00
J. Nick Koston
7f567bdfbe [fan] Add basic fan compile tests (#11484) 2025-10-23 10:53:15 +13:00
Daniel Stiner
f2de8df556 [openthread] Fix OTA by populating CORE.address with device's mDNS address (#11095)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-22 11:07:01 -10:00
J. Nick Koston
1c67a61945 [ci] Fix WiFi testing mode validation and component splitter for variant-only tests (#11481) 2025-10-23 08:10:24 +13:00
Jonathan Swoboda
77141d3e83 [esp32] Set the location of the IDF component manager cache (#11467) 2025-10-22 14:28:18 -04:00
J. Nick Koston
f592f79bce [ci] Fix component splitter for components with only variant tests (#11476) 2025-10-22 07:30:27 -10:00
J. Nick Koston
6edbb94529 [ci] Fix test detection for components with only variant tests (#11474) 2025-10-22 00:06:14 -10:00
Jeff Brown
d37eb59fd7 [light] Eliminate dimming undershoot during addressable light transition (#11471) 2025-10-22 08:22:33 +00:00
Jeff Brown
e2b3617df3 [climate] Fix restore state for fan mode, preset, and swing mode (#11126)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-21 22:08:40 -10:00
J. Nick Koston
e1c851cab8 [wifi] Optimize WiFi network storage with FixedVector (#11458)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-22 05:23:10 +00:00
J. Nick Koston
146b067d62 [light] Add compile test for addressable lights (#11465) 2025-10-22 16:59:39 +13:00
J. Nick Koston
5b15827009 [CI] Fix component detection when core files change in determine-jobs (#11461) 2025-10-22 16:58:40 +13:00
J. Nick Koston
0de79ba291 [event] Replace std::set with FixedVector for event type storage (#11463) 2025-10-22 16:57:18 +13:00
J. Nick Koston
e3aaf6a144 [wifi] Test multiple stas in wifi compile tests (#11460) 2025-10-22 16:55:46 +13:00
J. Nick Koston
78ffeb30fb [binary_sensor] Optimize MultiClickTrigger with FixedVector (#11453)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-22 16:55:13 +13:00
Jesse Hills
2c1927fd12 [api] Allow clearing noise psk if dynamically set (#11429) 2025-10-22 14:24:56 +13:00
Jesse Hills
c6ae1a5909 [core] Stop clang-format "fixing" a single line (#11462) 2025-10-22 01:00:27 +00:00
J. Nick Koston
9c712744be [light] Replace std::vector with FixedVector in strobe and color_wipe effects (#11455) 2025-10-22 11:40:19 +13:00
Javier Peletier
ae50a09b4e C++ components unit test framework (#9284)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 22:21:22 +00:00
Jeff Brown
1ea80594c6 [light] Improve gamma correction precision (#11141)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-21 22:11:11 +00:00
J. Nick Koston
8500323d39 [esp32] Add advanced options to disable unused VFS features (saves ~8.7 KB flash) (#11441) 2025-10-22 10:47:31 +13:00
J. Nick Koston
6f7db2f5f7 [gpio] Optimize switch interlock with FixedVector (#11448)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-21 11:35:34 -10:00
J. Nick Koston
9922c65912 Add compile tests for binary_sensor MultiClickTrigger (#11454)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-22 10:32:48 +13:00
J. Nick Koston
f2469077d9 [light] Add tests for AddressableColorWipeEffectColor/StrobeLightEffectColor (#11456) 2025-10-22 10:31:18 +13:00
Jesse Hills
742eca92d8 [CI] Add auto label for chained PRs (#11457) 2025-10-21 11:22:56 -10:00
J. Nick Koston
548913b471 Add gpio switch interlock compile tests (#11449) 2025-10-22 10:12:32 +13:00
Anton Sergunov
a05c5ea240 [uart] Make rx pin respect pullup and pulldown settings (#9248) 2025-10-22 10:10:25 +13:00
Jeff Brown
8e8a2bde95 [light] Decouple AddressableLight and Light transition classes (#11166)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 10:37:29 -10:00
Petr Kejval
80265a6bd2 [sensor] Add optimistic option to heartbeat filter (#10993)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 09:17:07 -04:00
J. Nick Koston
87e9a7a1bd [climate] Remove unnecessary vector allocations in state save/restore (#11445) 2025-10-21 04:35:18 -05:00
J. Nick Koston
3aedfe8be3 [binary_sensor] Optimize AutorepeatFilter with FixedVector (#11444) 2025-10-21 04:30:13 -05:00
J. Nick Koston
7f2cc47ed6 [binary_sensor] Add compile test for auto repeat (#11443) 2025-10-21 04:25:59 -05:00
J. Nick Koston
a5542e0d2b [sensor] Optimize calibration and Or filters with FixedVector (#11437) 2025-10-20 21:38:05 -10:00
Keith Burzinski
66afe4a9be [climate] Add some integration tests (#11439) 2025-10-21 02:26:18 -05:00
J. Nick Koston
0ae9009e41 [ci] Fix clang-tidy split mode for core file changes (#11434) 2025-10-20 20:39:50 -10:00
J. Nick Koston
0b2f5fcd7e Add additional sensor filter tests (#11438) 2025-10-20 20:39:21 -10:00
J. Nick Koston
7a2887e2ed [analyze-memory] Improve symbol categorization accuracy (#11440) 2025-10-20 20:39:05 -10:00
J. Nick Koston
cd2d3f061d [espnow] Fix compilation error with initializer_list after #11433 (#11436) 2025-10-20 19:58:24 -10:00
J. Nick Koston
73f5d01c2d [core] Optimize automation actions memory usage with std::initializer_list (#11433) 2025-10-21 04:32:58 +00:00
Jesse Hills
0938609f7a [improv] Put next_url behind defines to save flash (#11420)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 16:58:26 +13:00
J. Nick Koston
77203f0cb4 [text_sensor] Optimize filters with FixedVector (1.6KB flash savings) (#11423)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-21 03:24:51 +00:00
J. Nick Koston
040130e357 [ci] Fix memory impact workflow for new components (#11421) 2025-10-21 16:02:07 +13:00
J. Nick Koston
85959e3004 [sensor,text_sensor,binary_sensor] Optimize filter parameters with std::initializer_list (#11426) 2025-10-21 15:47:13 +13:00
Jonathan Swoboda
a809a13729 [core] Add support for extern "C" includes (#11422) 2025-10-21 15:46:50 +13:00
J. Nick Koston
3b6ff615e8 [ci] Fix clang-tidy split decision to account for component dependencies (#11430) 2025-10-21 15:39:15 +13:00
J. Nick Koston
05216db5f0 ESP8266: Complete testing mode memory patches with DRAM and Flash (#11427) 2025-10-21 15:26:49 +13:00
J. Nick Koston
9f668b0c4b Add basic text_sensor tests (#11424) 2025-10-21 15:26:41 +13:00
171 changed files with 4151 additions and 1109 deletions

View File

@@ -1 +1 @@
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
#include "esphome/core/log.h"
#include "absolute_humidity.h"
// test
namespace esphome {
namespace absolute_humidity {

View File

@@ -14,7 +14,6 @@ enum SaturationVaporPressureEquation {
};
/// This class implements calculation of absolute humidity from temperature and relative humidity.
// Test change for clang-tidy split logic
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
public:
AbsoluteHumidityComponent() = default;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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");

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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};

View File

@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
last_filter->next_ = filter;
}
}
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}

View File

@@ -4,7 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
#include <vector>
#include <initializer_list>
namespace esphome {
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
void publish_initial_state(bool new_state);
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void add_filters(std::initializer_list<Filter *> filters);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)

View File

@@ -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) {

View File

@@ -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};
};

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);

View File

@@ -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:

View File

@@ -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;

View File

@@ -550,6 +550,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 +641,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 +877,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 +1000,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(

View File

@@ -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]))

View File

@@ -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;

View File

@@ -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"),
)

View File

@@ -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)

View 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)

View File

@@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() {
#endif
// Acknowledge auth OK - 1 byte
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
// Read size, 4 bytes MSB first
if (!this->readall_(buf, 4)) {
this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
ota_size = 0;
for (uint8_t i = 0; i < 4; i++) {
ota_size <<= 8;
ota_size |= buf[i];
}
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
(static_cast<size_t>(buf[2]) << 8) | buf[3];
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
// Now that we've passed authentication and are actually
@@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() {
update_started = true;
// Acknowledge prepare OK - 1 byte
buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
// Read binary MD5, 32 bytes
if (!this->readall_(buf, 32)) {
@@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() {
this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
while (total < ota_size) {
// TODO: timeout check
@@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() {
total += read;
#if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
size_acknowledged += OTA_BLOCK_SIZE;
}
#endif
@@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge receive OK - 1 byte
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) {
@@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge Update end OK - 1 byte
buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
this->writeall_(buf, 1);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
// Read ACK
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
@@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() {
App.safe_reboot();
error:
buf[0] = static_cast<uint8_t>(error_code);
this->writeall_(buf, 1);
this->write_byte_(static_cast<uint8_t>(error_code));
this->cleanup_connection_();
if (this->backend_ != nullptr && update_started) {

View File

@@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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};
};

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@optimusprimespace", "@ssieb"]

View 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

View 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

View 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))

View File

@@ -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_{};

View File

@@ -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");

View File

@@ -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

View File

@@ -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");

View File

@@ -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:

View File

@@ -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");

View File

@@ -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_{};

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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();
}

View File

@@ -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_;

View File

@@ -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 {};
}

View File

@@ -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_{};
};

View File

@@ -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 &current_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 &current_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 &current_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 &current_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 &current_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 &current_color) override {
const uint32_t now = millis();
const uint8_t intensity = this->intensity_;

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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); }

View File

@@ -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_
}
}

View File

@@ -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_; }

View File

@@ -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();

View File

@@ -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};

View File

@@ -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;

View File

@@ -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

View 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

View File

@@ -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])

View File

@@ -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

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
),
}

View File

@@ -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))

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) {
}
filter->initialize(this, nullptr);
}
void Sensor::add_filters(const std::vector<Filter *> &filters) {
void Sensor::add_filters(std::initializer_list<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}
}
void Sensor::set_filters(const std::vector<Filter *> &filters) {
void Sensor::set_filters(std::initializer_list<Filter *> filters) {
this->clear_filters();
this->add_filters(filters);
}

View File

@@ -6,7 +6,7 @@
#include "esphome/core/log.h"
#include "esphome/components/sensor/filter.h"
#include <vector>
#include <initializer_list>
#include <memory>
namespace esphome {
@@ -77,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
* });
*/
void add_filters(const std::vector<Filter *> &filters);
void add_filters(std::initializer_list<Filter *> filters);
/// Clear the filters and replace them by filters.
void set_filters(const std::vector<Filter *> &filters);
void set_filters(std::initializer_list<Filter *> filters);
/// Clear the entire filter chain.
void clear_filters();

View File

@@ -110,17 +110,28 @@ def validate_mapping(value):
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
)
async def substitute_filter_to_code(config, filter_id):
from_strings = [conf[CONF_FROM] for conf in config]
to_strings = [conf[CONF_TO] for conf in config]
return cg.new_Pvariable(filter_id, from_strings, to_strings)
substitutions = [
cg.StructInitializer(
cg.MockObj("Substitution", "esphome::text_sensor::"),
("from", conf[CONF_FROM]),
("to", conf[CONF_TO]),
)
for conf in config
]
return cg.new_Pvariable(filter_id, substitutions)
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
async def map_filter_to_code(config, filter_id):
map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
return cg.new_Pvariable(
filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config])
)
mappings = [
cg.StructInitializer(
cg.MockObj("Substitution", "esphome::text_sensor::"),
("from", conf[CONF_FROM]),
("to", conf[CONF_TO]),
)
for conf in config
]
return cg.new_Pvariable(filter_id, mappings)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")

View File

@@ -62,19 +62,27 @@ optional<std::string> AppendFilter::new_value(std::string value) { return value
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
// Substitute
SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &substitutions)
: substitutions_(substitutions) {}
optional<std::string> SubstituteFilter::new_value(std::string value) {
std::size_t pos;
for (size_t i = 0; i < this->from_strings_.size(); i++) {
while ((pos = value.find(this->from_strings_[i])) != std::string::npos)
value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]);
for (const auto &sub : this->substitutions_) {
while ((pos = value.find(sub.from)) != std::string::npos)
value.replace(pos, sub.from.size(), sub.to);
}
return value;
}
// Map
MapFilter::MapFilter(const std::initializer_list<Substitution> &mappings) : mappings_(mappings) {}
optional<std::string> MapFilter::new_value(std::string value) {
auto item = mappings_.find(value);
return item == mappings_.end() ? value : item->second;
for (const auto &mapping : this->mappings_) {
if (mapping.from == value)
return mapping.to;
}
return value; // Pass through if no match
}
} // namespace text_sensor

View File

@@ -2,10 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <queue>
#include <utility>
#include <map>
#include <vector>
namespace esphome {
namespace text_sensor {
@@ -98,26 +94,52 @@ class PrependFilter : public Filter {
std::string prefix_;
};
struct Substitution {
std::string from;
std::string to;
};
/// A simple filter that replaces a substring with another substring
class SubstituteFilter : public Filter {
public:
SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings)
: from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {}
explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions);
optional<std::string> new_value(std::string value) override;
protected:
std::vector<std::string> from_strings_;
std::vector<std::string> to_strings_;
FixedVector<Substitution> substitutions_;
};
/// A filter that maps values from one set to another
/** A filter that maps values from one set to another
*
* Uses linear search instead of std::map for typical small datasets (2-20 mappings).
* Linear search on contiguous memory is faster than red-black tree lookups when:
* - Dataset is small (< ~30 items)
* - Memory is contiguous (cache-friendly, better CPU cache utilization)
* - No pointer chasing overhead (tree node traversal)
* - String comparison cost dominates lookup time
*
* Benchmark results (see benchmark_map_filter.cpp):
* - 2 mappings: Linear 1.26x faster than std::map
* - 5 mappings: Linear 2.25x faster than std::map
* - 10 mappings: Linear 1.83x faster than std::map
* - 20 mappings: Linear 1.59x faster than std::map
* - 30 mappings: Linear 1.09x faster than std::map
* - 40 mappings: std::map 1.27x faster than Linear (break-even)
*
* Benefits over std::map:
* - ~2KB smaller flash (no red-black tree code)
* - ~24-32 bytes less RAM per mapping (no tree node overhead)
* - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare)
*
* Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20
*/
class MapFilter : public Filter {
public:
MapFilter(std::map<std::string, std::string> mappings) : mappings_(std::move(mappings)) {}
explicit MapFilter(const std::initializer_list<Substitution> &mappings);
optional<std::string> new_value(std::string value) override;
protected:
std::map<std::string, std::string> mappings_;
FixedVector<Substitution> mappings_;
};
} // namespace text_sensor

View File

@@ -51,12 +51,12 @@ void TextSensor::add_filter(Filter *filter) {
}
filter->initialize(this, nullptr);
}
void TextSensor::add_filters(const std::vector<Filter *> &filters) {
void TextSensor::add_filters(std::initializer_list<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}
}
void TextSensor::set_filters(const std::vector<Filter *> &filters) {
void TextSensor::set_filters(std::initializer_list<Filter *> filters) {
this->clear_filters();
this->add_filters(filters);
}

View File

@@ -5,7 +5,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/text_sensor/filter.h"
#include <vector>
#include <initializer_list>
#include <memory>
namespace esphome {
@@ -37,10 +37,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void add_filter(Filter *filter);
/// Add a list of vectors to the back of the filter chain.
void add_filters(const std::vector<Filter *> &filters);
void add_filters(std::initializer_list<Filter *> filters);
/// Clear the filters and replace them by filters.
void set_filters(const std::vector<Filter *> &filters);
void set_filters(std::initializer_list<Filter *> filters);
/// Clear the entire filter chain.
void clear_filters();

View File

@@ -56,6 +56,13 @@ uint32_t ESP8266UartComponent::get_config() {
}
void ESP8266UartComponent::setup() {
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
// Use Arduino HardwareSerial UARTs if all used pins match the ones
// preconfigured by the platform. For example if RX disabled but TX pin
// is 1 we still want to use Serial.

View File

@@ -6,6 +6,9 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
@@ -104,6 +107,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;

View File

@@ -46,6 +46,13 @@ uint16_t LibreTinyUARTComponent::get_config() {
}
void LibreTinyUARTComponent::setup() {
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin();
int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin();
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();

View File

@@ -52,6 +52,13 @@ uint16_t RP2040UartComponent::get_config() {
}
void RP2040UartComponent::setup() {
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
uint16_t config = get_config();
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});

View File

@@ -213,11 +213,15 @@ def _validate(config):
if CONF_EAP in config:
network[CONF_EAP] = config.pop(CONF_EAP)
if CONF_NETWORKS in config:
raise cv.Invalid(
"You cannot use the 'ssid:' option together with 'networks:'. Please "
"copy your network into the 'networks:' key"
)
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
# In testing mode, merged component tests may have both ssid and networks
# Just use the networks list and ignore the single ssid
if not CORE.testing_mode:
raise cv.Invalid(
"You cannot use the 'ssid:' option together with 'networks:'. Please "
"copy your network into the 'networks:' key"
)
else:
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
if (CONF_NETWORKS not in config) and (CONF_AP not in config):
config = config.copy()
@@ -378,14 +382,19 @@ async def to_code(config):
# Track if any network uses Enterprise authentication
has_eap = False
def add_sta(ap, network):
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
# Initialize FixedVector with the count of networks
networks = config.get(CONF_NETWORKS, [])
if networks:
cg.add(var.init_sta(len(networks)))
for network in config.get(CONF_NETWORKS, []):
if CONF_EAP in network:
has_eap = True
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
def add_sta(ap: cg.MockObj, network: dict) -> None:
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
for network in networks:
if CONF_EAP in network:
has_eap = True
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
if CONF_AP in config:
conf = config[CONF_AP]

View File

@@ -330,9 +330,11 @@ float WiFiComponent::get_loop_priority() const {
return 10.0f; // before other loop components
}
void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
void WiFiComponent::set_sta(const WiFiAP &ap) {
this->clear_sta();
this->init_sta(1);
this->add_sta(ap);
}
void WiFiComponent::clear_sta() { this->sta_.clear(); }

View File

@@ -219,6 +219,7 @@ class WiFiComponent : public Component {
void set_sta(const WiFiAP &ap);
WiFiAP get_sta() { return this->selected_ap_; }
void init_sta(size_t count);
void add_sta(const WiFiAP &ap);
void clear_sta();
@@ -393,7 +394,7 @@ class WiFiComponent : public Component {
#endif
std::string use_address_;
std::vector<WiFiAP> sta_;
FixedVector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;
wifi_scan_vector_t<WiFiScanResult> scan_result_;
WiFiAP selected_ap_;

View File

@@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000;
static const char *const TAG = "wled_light_effect";
WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {}
WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {}
void WLEDLightEffect::start() {
AddressableLightEffect::start();

View File

@@ -15,7 +15,7 @@ namespace wled {
class WLEDLightEffect : public light::AddressableLightEffect {
public:
WLEDLightEffect(const std::string &name);
WLEDLightEffect(const char *name);
void start() override;
void stop() override;

View File

@@ -234,6 +234,9 @@ def copy_files():
"url": "https://esphome.io/",
"vendor": "esphome",
"build": {
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_fwid": "0x00B6"
}

View File

@@ -471,6 +471,7 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
CONF_INC_PIN = "inc_pin"
CONF_INCLUDE_INTERNAL = "include_internal"
CONF_INCLUDES = "includes"
CONF_INCLUDES_C = "includes_c"
CONF_INDEX = "index"
CONF_INDOOR = "indoor"
CONF_INFRARED = "infrared"

View File

@@ -636,11 +636,9 @@ class EsphomeCore:
if self.config is None:
raise ValueError("Config has not been loaded yet")
if CONF_WIFI in self.config:
return self.config[CONF_WIFI][CONF_USE_ADDRESS]
if CONF_ETHERNET in self.config:
return self.config[CONF_ETHERNET][CONF_USE_ADDRESS]
for network_type in (CONF_WIFI, CONF_ETHERNET, CONF_OPENTHREAD):
if network_type in self.config:
return self.config[network_type][CONF_USE_ADDRESS]
if CONF_OPENTHREAD in self.config:
return f"{self.name}.local"

Some files were not shown because too many files have changed in this diff Show More