diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 8a7a9428dbe..8b066064f24 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -175,6 +175,32 @@ ESP32_BOARD_PINS = { "LED": 13, "LED_BUILTIN": 13, }, + "adafruit_feather_esp32s3_reversetft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 38, + "TX": 39, + "SCL": 4, + "SDA": 3, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 21, + "TFT_I2C_POWER": 7, + "TFT_CS": 42, + "TFT_DC": 40, + "TFT_RESET": 41, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, "adafruit_feather_esp32s3_tft": { "BUTTON": 0, "A0": 18, diff --git a/esphome/components/hub75/boards/huidu.py b/esphome/components/hub75/boards/huidu.py index 52744d397e8..c4e4c3c1350 100644 --- a/esphome/components/hub75/boards/huidu.py +++ b/esphome/components/hub75/boards/huidu.py @@ -2,6 +2,25 @@ from . import BoardConfig +# Huidu HD-WF1 +BoardConfig( + "huidu-hd-wf1", + r1_pin=2, + g1_pin=6, + b1_pin=3, + r2_pin=4, + g2_pin=8, + b2_pin=5, + a_pin=39, + b_pin=38, + c_pin=37, + d_pin=36, + e_pin=12, + lat_pin=33, + oe_pin=35, + clk_pin=34, +) + # Huidu HD-WF2 BoardConfig( "huidu-hd-wf2", diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 07809023cdf..ca8d918441a 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() { int16_t ty = 0; int16_t td = 0; int16_t ts = 0; - int16_t angle = 0; + float angle = 0; uint8_t index = 0; Direction direction{DIRECTION_UNDEFINED}; bool is_moving = false; diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index 3dee8bf4703..ce58cedf117 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -143,6 +143,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( ], icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=1, ), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 9a1e1a109a1..410695da040 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -27,46 +27,61 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t } void RuntimeStatsCollector::log_stats_() { + // First pass: count active components + size_t count = 0; + for (const auto &it : this->component_stats_) { + if (it.second.get_period_count() > 0) { + count++; + } + } + ESP_LOGI(TAG, "Component Runtime Statistics\n" - " Period stats (last %" PRIu32 "ms):", - this->log_interval_); + " Period stats (last %" PRIu32 "ms): %zu active components", + this->log_interval_, count); - // First collect stats we want to display - std::vector stats_to_display; + if (count == 0) { + return; + } + // Stack buffer sized to actual active count (up to 256 components), heap fallback for larger + SmallBufferWithHeapFallback<256, Component *> buffer(count); + Component **sorted = buffer.get(); + + // Second pass: fill buffer with active components + size_t idx = 0; for (const auto &it : this->component_stats_) { - Component *component = it.first; - const ComponentRuntimeStats &stats = it.second; - if (stats.get_period_count() > 0) { - ComponentStatPair pair = {component, &stats}; - stats_to_display.push_back(pair); + if (it.second.get_period_count() > 0) { + sorted[idx++] = it.first; } } // Sort by period runtime (descending) - std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater()); + std::sort(sorted, sorted + count, [this](Component *a, Component *b) { + return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms(); + }); // Log top components by period runtime - for (const auto &it : stats_to_display) { + for (size_t i = 0; i < count; i++) { + const auto &stats = this->component_stats_[sorted[i]]; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), - it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(), + stats.get_period_max_time_ms(), stats.get_period_time_ms()); } - // Log total stats since boot - ESP_LOGI(TAG, " Total stats (since boot):"); + // Log total stats since boot (only for active components - idle ones haven't changed) + ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); // Re-sort by total runtime for all-time stats - std::sort(stats_to_display.begin(), stats_to_display.end(), - [](const ComponentStatPair &a, const ComponentStatPair &b) { - return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); - }); + std::sort(sorted, sorted + count, [this](Component *a, Component *b) { + return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms(); + }); - for (const auto &it : stats_to_display) { + for (size_t i = 0; i < count; i++) { + const auto &stats = this->component_stats_[sorted[i]]; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), - it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(), + stats.get_total_max_time_ms(), stats.get_total_time_ms()); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 56122364c22..c7fea7474b5 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -5,7 +5,6 @@ #ifdef USE_RUNTIME_STATS #include -#include #include #include #include "esphome/core/helpers.h" @@ -77,17 +76,6 @@ class ComponentRuntimeStats { uint32_t total_max_time_ms_; }; -// For sorting components by run time -struct ComponentStatPair { - Component *component; - const ComponentRuntimeStats *stats; - - bool operator>(const ComponentStatPair &other) const { - // Sort by period time as that's what we're displaying in the logs - return stats->get_period_time_ms() > other.stats->get_period_time_ms(); - } -}; - class RuntimeStatsCollector { public: RuntimeStatsCollector(); diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index f53a0a7cf71..8a78186178a 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -27,6 +27,9 @@ void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); #endif + auto time = this->now(); + ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ec9978da790..65c653a62a5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -39,6 +39,10 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif +#ifdef USE_IMPROV_SERIAL +#include "esphome/components/improv_serial/improv_serial_component.h" +#endif + namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -365,6 +369,75 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { return false; } +bool WiFiComponent::needs_full_scan_results_() const { + // Components that require full scan results (for example, scan result listeners) + // are expected to call request_wifi_scan_results(), which sets keep_scan_results_. + if (this->keep_scan_results_) { + return true; + } + +#ifdef USE_CAPTIVE_PORTAL + // Captive portal needs full results when active (showing network list to user) + if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) { + return true; + } +#endif + +#ifdef USE_IMPROV_SERIAL + // Improv serial needs results during provisioning (before connected) + if (improv_serial::global_improv_serial_component != nullptr && !this->is_connected()) { + return true; + } +#endif + +#ifdef USE_IMPROV + // BLE improv also needs results during provisioning + if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) { + return true; + } +#endif + + return false; +} + +bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const { + // Hidden networks in scan results have empty SSIDs - skip them + if (ssid[0] == '\0') { + return false; + } + for (const auto &sta : this->sta_) { + // Skip hidden network configs (they don't appear in normal scans) + if (sta.get_hidden()) { + continue; + } + // For BSSID-only configs (empty SSID), match by BSSID + if (sta.get_ssid().empty()) { + if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) { + return true; + } + continue; + } + // Match by SSID + if (sta.get_ssid() == ssid) { + return true; + } + } + return false; +} + +void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + // Skip logging during roaming scans to avoid log buffer overflow + // (roaming scans typically find many networks but only care about same-SSID APs) + if (this->roaming_state_ == RoamingState::SCANNING) { + return; + } + char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(bssid, bssid_s); + ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel); +#endif +} + int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { // Find next SSID to try in RETRY_HIDDEN phase. // @@ -656,8 +729,12 @@ void WiFiComponent::loop() { ESP_LOGI(TAG, "Starting fallback AP"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL - if (captive_portal::global_captive_portal != nullptr) + if (captive_portal::global_captive_portal != nullptr) { + // Reset so we force one full scan after captive portal starts + // (previous scans were filtered because captive portal wasn't active yet) + this->has_completed_scan_after_captive_portal_start_ = false; captive_portal::global_captive_portal->start(); + } #endif } } @@ -1195,7 +1272,7 @@ template static void insertion_sort_scan_results(VectorType // has overhead from UART transmission, so combining INFO+DEBUG into one line halves // the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls. __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { - char bssid_s[18]; + char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; auto bssid = res.get_bssid(); format_mac_addr_upper(bssid.data(), bssid_s); @@ -1211,18 +1288,6 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) #endif } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE -// Helper function to log non-matching scan results at verbose level -__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) { - char bssid_s[18]; - auto bssid = res.get_bssid(); - format_mac_addr_upper(bssid.data(), bssid_s); - - ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); -} -#endif - void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) { @@ -1232,6 +1297,8 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->has_completed_scan_after_captive_portal_start_ = + true; // Track that we've done a scan since captive portal started this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED; if (this->scan_result_.empty()) { @@ -1259,21 +1326,12 @@ void WiFiComponent::check_scanning_finished() { // Sort scan results using insertion sort for better memory efficiency insertion_sort_scan_results(this->scan_result_); - size_t non_matching_count = 0; + // Log matching networks (non-matching already logged at VERBOSE in scan callback) for (auto &res : this->scan_result_) { if (res.get_matches()) { log_scan_result(res); - } else { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - log_scan_result_non_matching(res); -#else - non_matching_count++; -#endif } } - if (non_matching_count > 0) { - ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count); - } // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config @@ -1532,7 +1590,10 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { if (this->went_through_explicit_hidden_phase_()) { return WiFiRetryPhase::EXPLICIT_HIDDEN; } - // Skip scanning when captive portal/improv is active to avoid disrupting AP. + // Skip scanning when captive portal/improv is active to avoid disrupting AP, + // BUT only if we've already completed at least one scan AFTER the portal started. + // When captive portal first starts, scan results may be filtered/stale, so we need + // to do one full scan to populate available networks for the captive portal UI. // // WHY SCANNING DISRUPTS AP MODE: // WiFi scanning requires the radio to leave the AP's channel and hop through @@ -1549,7 +1610,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { // // This allows users to configure WiFi via captive portal while the device keeps // attempting to connect to all configured networks in sequence. - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + // Captive portal needs scan results to show available networks. + // If captive portal is active, only skip scanning if we've done a scan after it started. + // If only improv is active (no captive portal), skip scanning since improv doesn't need results. + if (this->is_captive_portal_active_()) { + if (this->has_completed_scan_after_captive_portal_start_) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + // Need to scan for captive portal + } else if (this->is_esp32_improv_active_()) { + // Improv doesn't need scan results return WiFiRetryPhase::RETRY_HIDDEN; } return WiFiRetryPhase::SCAN_CONNECTING; @@ -2096,7 +2166,7 @@ void WiFiComponent::clear_roaming_state_() { void WiFiComponent::release_scan_results_() { if (!this->keep_scan_results_) { -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_ESP32) // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index dfc91fb5da8..f27c522a1bf 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -161,9 +161,12 @@ struct EAPAuth { using bssid_t = std::array; -// Use std::vector for RP2040 since scan count is unknown (callback-based) -// Use FixedVector for other platforms where count is queried first -#ifdef USE_RP2040 +/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID) +static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8; + +// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) +// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible +#if defined(USE_RP2040) || defined(USE_ESP32) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; @@ -539,6 +542,13 @@ class WiFiComponent : public Component { /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible bool ssid_was_seen_in_scan_(const std::string &ssid) const; + /// Check if full scan results are needed (captive portal active, improv, listeners) + bool needs_full_scan_results_() const; + /// Check if network matches any configured network (for scan result filtering) + /// Matches by SSID when configured, or by BSSID for BSSID-only configs + bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const; + /// Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow) + void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel); /// Find next SSID that wasn't in scan results (might be hidden) /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) @@ -710,6 +720,8 @@ class WiFiComponent : public Component { bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool has_completed_scan_after_captive_portal_start_{ + false}; // Tracks if we've completed a scan after captive portal started RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 91db7ae0eb9..be54038af8b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -760,20 +760,35 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { return; } - // Count the number of results first auto *head = reinterpret_cast(arg); + bool needs_full = this->needs_full_scan_results_(); + + // First pass: count matching networks (linked list is non-destructive) + size_t total = 0; size_t count = 0; for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { - count++; + total++; + const char *ssid_cstr = reinterpret_cast(it->ssid); + if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { + count++; + } } - this->scan_result_.init(count); + this->scan_result_.init(count); // Exact allocation + + // Second pass: store matching networks for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { - this->scan_result_.emplace_back( - bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, - std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, - it->is_hidden != 0); + const char *ssid_cstr = reinterpret_cast(it->ssid); + if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { + this->scan_result_.emplace_back( + bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, + std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); + } else { + this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel); + } } + ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), + needs_full ? "" : " (filtered)"); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS for (auto *listener : global_wifi_component->scan_results_listeners_) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 15fd407e3c2..a32232a7584 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -828,11 +828,21 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } uint16_t number = it.number; - scan_result_.init(number); + bool needs_full = this->needs_full_scan_results_(); + + // Smart reserve: full capacity if needed, small reserve otherwise + if (needs_full) { + this->scan_result_.reserve(number); + } else { + this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE); + } + #ifdef USE_ESP32_HOSTED // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor // Presumably an upstream bug, work-around by getting all records at once - auto records = std::make_unique(number); + // Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback + static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t); + SmallBufferWithHeapFallback records(number); err = esp_wifi_scan_get_ap_records(&number, records.get()); if (err != ESP_OK) { esp_wifi_clear_ap_list(); @@ -840,7 +850,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } for (uint16_t i = 0; i < number; i++) { - wifi_ap_record_t &record = records[i]; + wifi_ap_record_t &record = records.get()[i]; #else // Process one record at a time to avoid large buffer allocation for (uint16_t i = 0; i < number; i++) { @@ -852,12 +862,23 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { break; } #endif // USE_ESP32_HOSTED - bssid_t bssid; - std::copy(record.bssid, record.bssid + 6, bssid.begin()); - std::string ssid(reinterpret_cast(record.ssid)); - scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, - ssid.empty()); + + // Check C string first - avoid std::string construction for non-matching networks + const char *ssid_cstr = reinterpret_cast(record.ssid); + + // Only construct std::string and store if needed + if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + std::string ssid(ssid_cstr); + this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi, + record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); + } else { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + } } + ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), + needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS for (auto *listener : this->scan_results_listeners_) { listener->on_wifi_scan_results(this->scan_result_); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 20cd32fa8ff..af2b82c3c6b 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -670,18 +670,39 @@ void WiFiComponent::wifi_scan_done_callback_() { if (num < 0) return; - this->scan_result_.init(static_cast(num)); - for (int i = 0; i < num; i++) { - String ssid = WiFi.SSID(i); - wifi_auth_mode_t authmode = WiFi.encryptionType(i); - int32_t rssi = WiFi.RSSI(i); - uint8_t *bssid = WiFi.BSSID(i); - int32_t channel = WiFi.channel(i); + bool needs_full = this->needs_full_scan_results_(); - this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, - std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN, - ssid.length() == 0); + // Access scan results directly via WiFi.scan struct to avoid Arduino String allocations + // WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers + auto *scan = WiFi.scan; + + // First pass: count matching networks + size_t count = 0; + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) { + count++; + } } + + this->scan_result_.init(count); // Exact allocation + + // Second pass: store matching networks + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) { + auto &ap = scan->ap[i]; + this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], + ap.bssid.addr[4], ap.bssid.addr[5]}, + std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, + ssid_cstr[0] == '\0'); + } else { + auto &ap = scan->ap[i]; + this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel); + } + } + ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(), + needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS for (auto *listener : this->scan_results_listeners_) { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 29ac096d944..84c10d5d432 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -21,6 +21,7 @@ static const char *const TAG = "wifi_pico_w"; // Track previous state for detecting changes static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static size_t s_scan_result_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { @@ -137,10 +138,20 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r } void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + s_scan_result_count++; + const char *ssid_cstr = reinterpret_cast(result->ssid); + + // Skip networks that don't match any configured network (unless full results needed) + if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) { + this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel); + return; + } + bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - std::string ssid(reinterpret_cast(result->ssid)); - WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty()); + std::string ssid(ssid_cstr); + WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, + ssid_cstr[0] == '\0'); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } @@ -149,6 +160,7 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re bool WiFiComponent::wifi_scan_start_(bool passive) { this->scan_result_.clear(); this->scan_done_ = false; + s_scan_result_count = 0; cyw43_wifi_scan_options_t scan_options = {0}; scan_options.scan_type = passive ? 1 : 0; int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); @@ -244,7 +256,9 @@ void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; - ESP_LOGV(TAG, "Scan done"); + bool needs_full = this->needs_full_scan_results_(); + ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), + needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS for (auto *listener : this->scan_results_listeners_) { listener->on_wifi_scan_results(this->scan_result_); diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 2281dd38a9e..1b1da783084 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -12,6 +12,7 @@ from esphome.core import CORE from esphome.types import ConfigType from .const_zephyr import ( + CONF_IEEE802154_VENDOR_OUI, CONF_MAX_EP_NUMBER, CONF_ON_JOIN, CONF_POWER_SOURCE, @@ -58,6 +59,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum( POWER_SOURCE, upper=True ), + cv.Optional(CONF_IEEE802154_VENDOR_OUI): cv.All( + cv.Any( + cv.int_range(min=0x000000, max=0xFFFFFF), + cv.one_of(*["random"], lower=True), + ), + cv.requires_component("nrf52"), + ), } ).extend(cv.COMPONENT_SCHEMA), zigbee_set_core_data, @@ -120,7 +128,7 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None: def consume_endpoint(config: ConfigType) -> ConfigType: if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): return config - if " " in config[CONF_NAME]: + if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", config[CONF_NAME], diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 0372f225938..03c1bb546f6 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -22,6 +22,7 @@ POWER_SOURCE = { "EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST", "EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF", } +CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui" # Keys for CORE.data storage KEY_ZIGBEE = "zigbee" diff --git a/esphome/components/zigbee/time/__init__.py b/esphome/components/zigbee/time/__init__.py new file mode 100644 index 00000000000..82f94c83721 --- /dev/null +++ b/esphome/components/zigbee/time/__init__.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components import time as time_ +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.types import ConfigType + +from .. import consume_endpoint +from ..const_zephyr import CONF_ZIGBEE_ID, zigbee_ns +from ..zigbee_zephyr import ( + ZigbeeClusterDesc, + ZigbeeComponent, + get_slot_index, + zigbee_new_attr_list, + zigbee_new_cluster_list, + zigbee_new_variable, + zigbee_register_ep, +) + +DEPENDENCIES = ["zigbee"] + +ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock) + +CONFIG_SCHEMA = cv.All( + time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ZigbeeTime), + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id( + ZigbeeComponent + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("1s")), + consume_endpoint, +) + + +async def to_code(config: ConfigType) -> None: + CORE.add_job(_add_time, config) + + +async def _add_time(config: ConfigType) -> None: + slot_index = get_slot_index() + + # Create unique names for this sensor's variables based on slot index + prefix = f"zigbee_ep{slot_index + 1}" + attrs_name = f"{prefix}_time_attrs" + attr_list_name = f"{prefix}_time_attrib_list" + cluster_list_name = f"{prefix}_cluster_list" + ep_name = f"{prefix}_ep" + + # Create the binary attributes structure + time_attrs = zigbee_new_variable(attrs_name, "zb_zcl_time_attrs_t") + attr_list = zigbee_new_attr_list( + attr_list_name, + "ZB_ZCL_DECLARE_TIME_ATTR_LIST", + str(time_attrs), + ) + + # Create cluster list and register endpoint + cluster_list_name, clusters = zigbee_new_cluster_list( + cluster_list_name, + [ + ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME", attr_list), + ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME"), + ], + ) + zigbee_register_ep( + ep_name, + cluster_list_name, + 0, + clusters, + slot_index, + "ZB_HA_CUSTOM_ATTR_DEVICE_ID", + ) + + # Create the ZigbeeTime component + var = cg.new_Pvariable(config[CONF_ID]) + await time_.register_time(var, config) + await cg.register_component(var, config) + + cg.add(var.set_endpoint(slot_index + 1)) + cg.add(var.set_cluster_attributes(time_attrs)) + hub = await cg.get_variable(config[CONF_ZIGBEE_ID]) + cg.add(var.set_parent(hub)) diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp new file mode 100644 index 00000000000..70ceb60abee --- /dev/null +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp @@ -0,0 +1,87 @@ +#include "zigbee_time_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) +#include "esphome/core/log.h" + +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.time"; + +// This time standard is the number of +// seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time). +constexpr time_t EPOCH_2000 = 946684800; + +ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +void ZigbeeTime::sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint, + zb_uint32_t nw_time) { + if (status == RET_OK && auth_level >= ZB_ZCL_TIME_HAS_SYNCHRONIZED_BIT) { + global_time->set_epoch_time(nw_time + EPOCH_2000); + } else if (status != RET_TIMEOUT || !global_time->has_time_) { + ESP_LOGE(TAG, "Status: %d, auth_level: %u, short_addr: %d, endpoint: %d, nw_time: %u", status, auth_level, + short_addr, endpoint, nw_time); + } +} + +void ZigbeeTime::setup() { + global_time = this; + this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); + synchronize_epoch_(EPOCH_2000); + this->parent_->add_join_callback([this]() { zb_zcl_time_server_synchronize(this->endpoint_, sync_time); }); +} + +void ZigbeeTime::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Time\n" + " Endpoint: %d", + this->endpoint_); + RealTimeClock::dump_config(); +} + +void ZigbeeTime::update() { + time_t time = timestamp_now(); + this->cluster_attributes_->time = time - EPOCH_2000; +} + +void ZigbeeTime::set_epoch_time(uint32_t epoch) { + this->defer([this, epoch]() { + this->synchronize_epoch_(epoch); + this->has_time_ = true; + }); +} + +void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + + switch (device_cb_id) { + /* ZCL set attribute value */ + case ZB_ZCL_SET_ATTR_VALUE_CB_ID: + if (cluster_id == ZB_ZCL_CLUSTER_ID_TIME) { + if (attr_id == ZB_ZCL_ATTR_TIME_TIME_ID) { + zb_uint32_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data32; + ESP_LOGI(TAG, "Synchronize time to %u", value); + this->defer([this, value]() { synchronize_epoch_(value + EPOCH_2000); }); + } else if (attr_id == ZB_ZCL_ATTR_TIME_TIME_STATUS_ID) { + zb_uint8_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data8; + ESP_LOGI(TAG, "Time status %hd", value); + this->defer([this, value]() { this->has_time_ = ZB_ZCL_TIME_TIME_STATUS_SYNCHRONIZED_BIT_IS_SET(value); }); + } + } else { + /* other clusters attribute handled here */ + ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + } + break; + default: + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + break; + } + + ESP_LOGD(TAG, "Zcl_device_cb_ status: %hd", p_device_cb_param->status); +} + +} // namespace esphome::zigbee + +#endif diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.h b/esphome/components/zigbee/time/zigbee_time_zephyr.h new file mode 100644 index 00000000000..3c2adc4b5fa --- /dev/null +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.h @@ -0,0 +1,38 @@ +#pragma once +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) +#include "esphome/core/component.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/components/zigbee/zigbee_zephyr.h" + +extern "C" { +#include +#include +} + +namespace esphome::zigbee { + +class ZigbeeTime : public time::RealTimeClock, public ZigbeeEntity { + public: + void setup() override; + void dump_config() override; + void update() override; + + void set_cluster_attributes(zb_zcl_time_attrs_t &cluster_attributes) { + this->cluster_attributes_ = &cluster_attributes; + } + + void set_epoch_time(uint32_t epoch); + + protected: + static void sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint, + zb_uint32_t nw_time); + void zcl_device_cb_(zb_bufid_t bufid); + zb_zcl_time_attrs_t *cluster_attributes_{nullptr}; + + bool has_time_{false}; +}; + +} // namespace esphome::zigbee + +#endif diff --git a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp index 8b7aff70a82..464cc04d620 100644 --- a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp @@ -22,7 +22,7 @@ void ZigbeeBinarySensor::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp index 74550d6487f..25e1e083e00 100644 --- a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp @@ -21,7 +21,7 @@ void ZigbeeSensor::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, (zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } diff --git a/esphome/components/zigbee/zigbee_switch_zephyr.cpp b/esphome/components/zigbee/zigbee_switch_zephyr.cpp index 5454f262f9e..fef02e5a0c2 100644 --- a/esphome/components/zigbee/zigbee_switch_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_switch_zephyr.cpp @@ -31,7 +31,7 @@ void ZigbeeSwitch::setup() { ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value, ZB_FALSE); - this->parent_->flush(); + this->parent_->force_report(); }); } @@ -41,8 +41,6 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; - p_device_cb_param->status = RET_OK; - switch (device_cb_id) { /* ZCL set attribute value */ case ZB_ZCL_SET_ATTR_VALUE_CB_ID: @@ -58,10 +56,11 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { } else { /* other clusters attribute handled here */ ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; } break; default: - p_device_cb_param->status = RET_ERROR; + p_device_cb_param->status = RET_NOT_IMPLEMENTED; break; } diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index e43ab8f84d6..eabf5c30d40 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -112,10 +112,10 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { const auto &cb = global_zigbee->callbacks_[endpoint - 1]; if (cb) { cb(bufid); + return; } - return; } - p_device_cb_param->status = RET_ERROR; + p_device_cb_param->status = RET_NOT_IMPLEMENTED; } void ZigbeeComponent::on_join_() { @@ -230,11 +230,11 @@ static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { zb_buf_free(bufid); } -void ZigbeeComponent::flush() { this->need_flush_ = true; } +void ZigbeeComponent::force_report() { this->force_report_ = true; } void ZigbeeComponent::loop() { - if (this->need_flush_) { - this->need_flush_ = false; + if (this->force_report_) { + this->force_report_ = false; zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); } } diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index d5f1257f9c4..b75c192c1a1 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -72,7 +72,7 @@ class ZigbeeComponent : public Component { void zboss_signal_handler_esphome(zb_bufid_t bufid); void factory_reset(); Trigger<> *get_join_trigger() { return &this->join_trigger_; }; - void flush(); + void force_report(); void loop() override; protected: @@ -84,7 +84,7 @@ class ZigbeeComponent : public Component { std::array, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; CallbackManager join_cb_; Trigger<> join_trigger_; - bool need_flush_{false}; + bool force_report_{false}; }; class ZigbeeEntity { diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 7f1f7dc57f5..b3bd10bfabd 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -49,6 +49,7 @@ from esphome.cpp_generator import ( from esphome.types import ConfigType from .const_zephyr import ( + CONF_IEEE802154_VENDOR_OUI, CONF_ON_JOIN, CONF_POWER_SOURCE, CONF_WIPE_ON_BOOT, @@ -152,6 +153,13 @@ async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) zephyr_add_prj_conf("NET_UDP", False) + if CONF_IEEE802154_VENDOR_OUI in config: + zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True) + random_number = config[CONF_IEEE802154_VENDOR_OUI] + if random_number == "random": + random_number = random.randint(0x000000, 0xFFFFFF) + zephyr_add_prj_conf("IEEE802154_VENDOR_OUI", random_number) + if config[CONF_WIPE_ON_BOOT]: if config[CONF_WIPE_ON_BOOT] == "once": cg.add_define( @@ -336,14 +344,14 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None: CORE.add_job(_add_switch, entity, config) -def _slot_index() -> int: - """Find the next available endpoint slot""" +def get_slot_index() -> int: + """Find the next available endpoint slot.""" slot = next( (i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None ) if slot is None: raise cv.Invalid( - f"Not found empty slot, size ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})" + f"No available Zigbee endpoint slots ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])} in use)" ) return slot @@ -358,7 +366,7 @@ async def _add_zigbee_ep( app_device_id: str, extra_field_values: dict[str, int] | None = None, ) -> None: - slot_index = _slot_index() + slot_index = get_slot_index() prefix = f"zigbee_ep{slot_index + 1}" attrs_name = f"{prefix}_attrs" diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6c5904ef255..9c7060cd1d5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -16,7 +16,6 @@ #include #include #include - #include #include "esphome/core/optional.h" diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index 11100e1e0c2..e4dee5f74a5 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -8,8 +8,6 @@ binary_sensor: name: "Garage Door Open 3" - platform: template name: "Garage Door Open 4" - - platform: template - name: "Garage Door Open 5" - platform: template name: "Garage Door Internal" internal: True @@ -21,6 +19,10 @@ sensor: - platform: template name: "Analog 2" lambda: return 11.0; + - platform: template + name: "Analog 3" + lambda: return 12.0; + internal: True zigbee: wipe_on_boot: true @@ -35,6 +37,9 @@ output: write_action: - zigbee.factory_reset +time: + - platform: zigbee + switch: - platform: template name: "Template Switch" diff --git a/tests/components/zigbee/test.nrf52-xiao-ble.yaml b/tests/components/zigbee/test.nrf52-xiao-ble.yaml index d2ce552de33..254f370ca7f 100644 --- a/tests/components/zigbee/test.nrf52-xiao-ble.yaml +++ b/tests/components/zigbee/test.nrf52-xiao-ble.yaml @@ -3,3 +3,4 @@ zigbee: wipe_on_boot: once power_source: battery + ieee802154_vendor_oui: 0x231