mirror of
https://github.com/esphome/esphome.git
synced 2025-10-23 20:23:50 +01:00
Merge branch 'fast_connect_cond_compile' into integration
This commit is contained in:
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
@@ -407,7 +407,8 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||||
cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
|
if config[CONF_FAST_CONNECT]:
|
||||||
|
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||||
if CONF_OUTPUT_POWER in config:
|
if CONF_OUTPUT_POWER in config:
|
||||||
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
||||||
|
@@ -84,9 +84,9 @@ void WiFiComponent::start() {
|
|||||||
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
|
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
|
||||||
|
|
||||||
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
|
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
|
||||||
if (this->fast_connect_) {
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
|
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
|
||||||
}
|
#endif
|
||||||
|
|
||||||
SavedWifiSettings save{};
|
SavedWifiSettings save{};
|
||||||
if (this->pref_.load(&save)) {
|
if (this->pref_.load(&save)) {
|
||||||
@@ -108,16 +108,16 @@ void WiFiComponent::start() {
|
|||||||
ESP_LOGV(TAG, "Setting Power Save Option failed");
|
ESP_LOGV(TAG, "Setting Power Save Option failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->fast_connect_) {
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
this->trying_loaded_ap_ = this->load_fast_connect_settings_();
|
this->trying_loaded_ap_ = this->load_fast_connect_settings_();
|
||||||
if (!this->trying_loaded_ap_) {
|
if (!this->trying_loaded_ap_) {
|
||||||
this->ap_index_ = 0;
|
this->ap_index_ = 0;
|
||||||
this->selected_ap_ = this->sta_[this->ap_index_];
|
this->selected_ap_ = this->sta_[this->ap_index_];
|
||||||
}
|
|
||||||
this->start_connecting(this->selected_ap_, false);
|
|
||||||
} else {
|
|
||||||
this->start_scanning();
|
|
||||||
}
|
}
|
||||||
|
this->start_connecting(this->selected_ap_, false);
|
||||||
|
#else
|
||||||
|
this->start_scanning();
|
||||||
|
#endif
|
||||||
#ifdef USE_WIFI_AP
|
#ifdef USE_WIFI_AP
|
||||||
} else if (this->has_ap()) {
|
} else if (this->has_ap()) {
|
||||||
this->setup_ap_config_();
|
this->setup_ap_config_();
|
||||||
@@ -168,13 +168,19 @@ void WiFiComponent::loop() {
|
|||||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||||
if (millis() - this->action_started_ > 5000) {
|
if (millis() - this->action_started_ > 5000) {
|
||||||
if (this->fast_connect_ || this->retry_hidden_) {
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
|
if (!this->selected_ap_.get_bssid().has_value())
|
||||||
|
this->selected_ap_ = this->sta_[0];
|
||||||
|
this->start_connecting(this->selected_ap_, false);
|
||||||
|
#else
|
||||||
|
if (!this->retry_hidden_) {
|
||||||
|
this->start_scanning();
|
||||||
|
} else {
|
||||||
if (!this->selected_ap_.get_bssid().has_value())
|
if (!this->selected_ap_.get_bssid().has_value())
|
||||||
this->selected_ap_ = this->sta_[0];
|
this->selected_ap_ = this->sta_[0];
|
||||||
this->start_connecting(this->selected_ap_, false);
|
this->start_connecting(this->selected_ap_, false);
|
||||||
} else {
|
|
||||||
this->start_scanning();
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -244,7 +250,6 @@ WiFiComponent::WiFiComponent() { global_wifi_component = this; }
|
|||||||
|
|
||||||
bool WiFiComponent::has_ap() const { return this->has_ap_; }
|
bool WiFiComponent::has_ap() const { return this->has_ap_; }
|
||||||
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
|
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
|
||||||
void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; }
|
|
||||||
#ifdef USE_WIFI_11KV_SUPPORT
|
#ifdef USE_WIFI_11KV_SUPPORT
|
||||||
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
|
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
|
||||||
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
|
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
|
||||||
@@ -721,9 +726,9 @@ void WiFiComponent::check_connecting_finished() {
|
|||||||
this->scan_result_.shrink_to_fit();
|
this->scan_result_.shrink_to_fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->fast_connect_) {
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
this->save_fast_connect_settings_();
|
this->save_fast_connect_settings_();
|
||||||
}
|
#endif
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -771,31 +776,31 @@ void WiFiComponent::retry_connect() {
|
|||||||
delay(10);
|
delay(10);
|
||||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
|
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
|
||||||
(this->num_retried_ > 3 || this->error_from_callback_)) {
|
(this->num_retried_ > 3 || this->error_from_callback_)) {
|
||||||
if (this->fast_connect_) {
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
if (this->trying_loaded_ap_) {
|
if (this->trying_loaded_ap_) {
|
||||||
this->trying_loaded_ap_ = false;
|
this->trying_loaded_ap_ = false;
|
||||||
this->ap_index_ = 0; // Retry from the first configured AP
|
this->ap_index_ = 0; // Retry from the first configured AP
|
||||||
} else if (this->ap_index_ >= this->sta_.size() - 1) {
|
} else if (this->ap_index_ >= this->sta_.size() - 1) {
|
||||||
ESP_LOGW(TAG, "No more APs to try");
|
ESP_LOGW(TAG, "No more APs to try");
|
||||||
this->ap_index_ = 0;
|
this->ap_index_ = 0;
|
||||||
this->restart_adapter();
|
this->restart_adapter();
|
||||||
} else {
|
|
||||||
// Try next AP
|
|
||||||
this->ap_index_++;
|
|
||||||
}
|
|
||||||
this->num_retried_ = 0;
|
|
||||||
this->selected_ap_ = this->sta_[this->ap_index_];
|
|
||||||
} else {
|
} else {
|
||||||
if (this->num_retried_ > 5) {
|
// Try next AP
|
||||||
// If retry failed for more than 5 times, let's restart STA
|
this->ap_index_++;
|
||||||
this->restart_adapter();
|
|
||||||
} else {
|
|
||||||
// Try hidden networks after 3 failed retries
|
|
||||||
ESP_LOGD(TAG, "Retrying with hidden networks");
|
|
||||||
this->retry_hidden_ = true;
|
|
||||||
this->num_retried_++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this->num_retried_ = 0;
|
||||||
|
this->selected_ap_ = this->sta_[this->ap_index_];
|
||||||
|
#else
|
||||||
|
if (this->num_retried_ > 5) {
|
||||||
|
// If retry failed for more than 5 times, let's restart STA
|
||||||
|
this->restart_adapter();
|
||||||
|
} else {
|
||||||
|
// Try hidden networks after 3 failed retries
|
||||||
|
ESP_LOGD(TAG, "Retrying with hidden networks");
|
||||||
|
this->retry_hidden_ = true;
|
||||||
|
this->num_retried_++;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
this->num_retried_++;
|
this->num_retried_++;
|
||||||
}
|
}
|
||||||
@@ -841,6 +846,7 @@ bool WiFiComponent::is_esp32_improv_active_() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
bool WiFiComponent::load_fast_connect_settings_() {
|
bool WiFiComponent::load_fast_connect_settings_() {
|
||||||
SavedWifiFastConnectSettings fast_connect_save{};
|
SavedWifiFastConnectSettings fast_connect_save{};
|
||||||
|
|
||||||
@@ -875,6 +881,7 @@ void WiFiComponent::save_fast_connect_settings_() {
|
|||||||
ESP_LOGD(TAG, "Saved fast_connect settings");
|
ESP_LOGD(TAG, "Saved fast_connect settings");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||||
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
|
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
|
||||||
|
@@ -240,7 +240,6 @@ class WiFiComponent : public Component {
|
|||||||
void start_scanning();
|
void start_scanning();
|
||||||
void check_scanning_finished();
|
void check_scanning_finished();
|
||||||
void start_connecting(const WiFiAP &ap, bool two);
|
void start_connecting(const WiFiAP &ap, bool two);
|
||||||
void set_fast_connect(bool fast_connect);
|
|
||||||
void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
|
void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
|
||||||
|
|
||||||
void check_connecting_finished();
|
void check_connecting_finished();
|
||||||
@@ -364,8 +363,10 @@ class WiFiComponent : public Component {
|
|||||||
bool is_captive_portal_active_();
|
bool is_captive_portal_active_();
|
||||||
bool is_esp32_improv_active_();
|
bool is_esp32_improv_active_();
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
bool load_fast_connect_settings_();
|
bool load_fast_connect_settings_();
|
||||||
void save_fast_connect_settings_();
|
void save_fast_connect_settings_();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
static void wifi_event_callback(System_Event_t *event);
|
static void wifi_event_callback(System_Event_t *event);
|
||||||
@@ -399,7 +400,9 @@ class WiFiComponent : public Component {
|
|||||||
WiFiAP ap_;
|
WiFiAP ap_;
|
||||||
optional<float> output_power_;
|
optional<float> output_power_;
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
ESPPreferenceObject fast_connect_pref_;
|
ESPPreferenceObject fast_connect_pref_;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Group all 32-bit integers together
|
// Group all 32-bit integers together
|
||||||
uint32_t action_started_;
|
uint32_t action_started_;
|
||||||
@@ -417,8 +420,9 @@ class WiFiComponent : public Component {
|
|||||||
#endif /* USE_NETWORK_IPV6 */
|
#endif /* USE_NETWORK_IPV6 */
|
||||||
|
|
||||||
// Group all boolean values together
|
// Group all boolean values together
|
||||||
bool fast_connect_{false};
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
bool trying_loaded_ap_{false};
|
bool trying_loaded_ap_{false};
|
||||||
|
#endif
|
||||||
bool retry_hidden_{false};
|
bool retry_hidden_{false};
|
||||||
bool has_ap_{false};
|
bool has_ap_{false};
|
||||||
bool handled_connected_state_{false};
|
bool handled_connected_state_{false};
|
||||||
|
@@ -199,6 +199,7 @@
|
|||||||
#define USE_WEBSERVER_PORT 80 // NOLINT
|
#define USE_WEBSERVER_PORT 80 // NOLINT
|
||||||
#define USE_WEBSERVER_SORTING
|
#define USE_WEBSERVER_SORTING
|
||||||
#define USE_WIFI_11KV_SUPPORT
|
#define USE_WIFI_11KV_SUPPORT
|
||||||
|
#define USE_WIFI_FAST_CONNECT
|
||||||
#define USB_HOST_MAX_REQUESTS 16
|
#define USB_HOST_MAX_REQUESTS 16
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
@@ -10,6 +10,10 @@ from esphome.helpers import get_bool_env
|
|||||||
|
|
||||||
from .util.password import password_hash
|
from .util.password import password_hash
|
||||||
|
|
||||||
|
# Sentinel file name used for CORE.config_path when dashboard initializes.
|
||||||
|
# This ensures .parent returns the config directory instead of root.
|
||||||
|
_DASHBOARD_SENTINEL_FILE = "___DASHBOARD_SENTINEL___.yaml"
|
||||||
|
|
||||||
|
|
||||||
class DashboardSettings:
|
class DashboardSettings:
|
||||||
"""Settings for the dashboard."""
|
"""Settings for the dashboard."""
|
||||||
@@ -48,7 +52,12 @@ class DashboardSettings:
|
|||||||
self.config_dir = Path(args.configuration)
|
self.config_dir = Path(args.configuration)
|
||||||
self.absolute_config_dir = self.config_dir.resolve()
|
self.absolute_config_dir = self.config_dir.resolve()
|
||||||
self.verbose = args.verbose
|
self.verbose = args.verbose
|
||||||
CORE.config_path = self.config_dir / "."
|
# Set to a sentinel file so .parent gives us the config directory.
|
||||||
|
# Previously this was `os.path.join(self.config_dir, ".")` which worked because
|
||||||
|
# os.path.dirname("/config/.") returns "/config", but Path("/config/.").parent
|
||||||
|
# normalizes to Path("/config") first, then .parent returns Path("/"), breaking
|
||||||
|
# secret resolution. Using a sentinel file ensures .parent gives the correct directory.
|
||||||
|
CORE.config_path = self.config_dir / _DASHBOARD_SENTINEL_FILE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relative_url(self) -> str:
|
def relative_url(self) -> str:
|
||||||
|
@@ -83,8 +83,6 @@ ISOLATED_COMPONENTS = {
|
|||||||
"openthread": "Conflicts with wifi: used by most components",
|
"openthread": "Conflicts with wifi: used by most components",
|
||||||
"openthread_info": "Conflicts with wifi: used by most components",
|
"openthread_info": "Conflicts with wifi: used by most components",
|
||||||
"matrix_keypad": "Needs isolation due to keypad",
|
"matrix_keypad": "Needs isolation due to keypad",
|
||||||
"mcp4725": "no YAML config to specify i2c bus id",
|
|
||||||
"mcp47a1": "no YAML config to specify i2c bus id",
|
|
||||||
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
|
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
|
||||||
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
|
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
|
||||||
"packages": "cannot merge packages",
|
"packages": "cannot merge packages",
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
output:
|
output:
|
||||||
- platform: mcp4725
|
- platform: mcp4725
|
||||||
id: mcp4725_dac_output
|
id: mcp4725_dac_output
|
||||||
|
i2c_id: i2c_bus
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
output:
|
output:
|
||||||
- platform: mcp47a1
|
- platform: mcp47a1
|
||||||
id: output_mcp47a1
|
id: output_mcp47a1
|
||||||
|
i2c_id: i2c_bus
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
wifi:
|
wifi:
|
||||||
|
fast_connect: true
|
||||||
networks:
|
networks:
|
||||||
- ssid: MySSID
|
- ssid: MySSID
|
||||||
eap:
|
eap:
|
||||||
|
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from argparse import Namespace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from esphome.core import CORE
|
||||||
from esphome.dashboard.settings import DashboardSettings
|
from esphome.dashboard.settings import DashboardSettings
|
||||||
|
|
||||||
|
|
||||||
@@ -159,3 +161,63 @@ def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> No
|
|||||||
result = dashboard_settings.rel_path("123", "456.789")
|
result = dashboard_settings.rel_path("123", "456.789")
|
||||||
expected = dashboard_settings.config_dir / "123" / "456.789"
|
expected = dashboard_settings.config_dir / "123" / "456.789"
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None:
|
||||||
|
"""Test that CORE.config_path.parent resolves to config_dir after parse_args.
|
||||||
|
|
||||||
|
This is a regression test for issue #11280 where binary download failed
|
||||||
|
when using packages with secrets after the Path migration in 2025.10.0.
|
||||||
|
|
||||||
|
The issue was that after switching from os.path to Path:
|
||||||
|
- Before: os.path.dirname("/config/.") → "/config"
|
||||||
|
- After: Path("/config/.").parent → Path("/") (normalized first!)
|
||||||
|
|
||||||
|
The fix uses a sentinel file so .parent returns the correct directory:
|
||||||
|
- Fixed: Path("/config/___DASHBOARD_SENTINEL___.yaml").parent → Path("/config")
|
||||||
|
"""
|
||||||
|
# Create test directory structure with secrets and packages
|
||||||
|
config_dir = tmp_path / "config"
|
||||||
|
config_dir.mkdir()
|
||||||
|
|
||||||
|
# Create secrets.yaml with obviously fake test values
|
||||||
|
secrets_file = config_dir / "secrets.yaml"
|
||||||
|
secrets_file.write_text(
|
||||||
|
"wifi_ssid: TEST-DUMMY-SSID\n"
|
||||||
|
"wifi_password: not-a-real-password-just-for-testing\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create package file that uses secrets
|
||||||
|
package_file = config_dir / "common.yaml"
|
||||||
|
package_file.write_text(
|
||||||
|
"wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create main device config that includes the package
|
||||||
|
device_config = config_dir / "test-device.yaml"
|
||||||
|
device_config.write_text(
|
||||||
|
"esphome:\n name: test-device\n\npackages:\n common: !include common.yaml\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up dashboard settings with our test config directory
|
||||||
|
settings = DashboardSettings()
|
||||||
|
args = Namespace(
|
||||||
|
configuration=str(config_dir),
|
||||||
|
password=None,
|
||||||
|
username=None,
|
||||||
|
ha_addon=False,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
settings.parse_args(args)
|
||||||
|
|
||||||
|
# Verify that CORE.config_path.parent correctly points to the config directory
|
||||||
|
# This is critical for secret resolution in yaml_util.py which does:
|
||||||
|
# main_config_dir = CORE.config_path.parent
|
||||||
|
# main_secret_yml = main_config_dir / "secrets.yaml"
|
||||||
|
assert CORE.config_path.parent == config_dir.resolve()
|
||||||
|
assert (CORE.config_path.parent / "secrets.yaml").exists()
|
||||||
|
assert (CORE.config_path.parent / "common.yaml").exists()
|
||||||
|
|
||||||
|
# Verify that CORE.config_path itself uses the sentinel file
|
||||||
|
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
||||||
|
assert not CORE.config_path.exists() # Sentinel file doesn't actually exist
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from argparse import Namespace
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
@@ -17,6 +18,8 @@ from tornado.ioloop import IOLoop
|
|||||||
from tornado.testing import bind_unused_port
|
from tornado.testing import bind_unused_port
|
||||||
from tornado.websocket import WebSocketClientConnection, websocket_connect
|
from tornado.websocket import WebSocketClientConnection, websocket_connect
|
||||||
|
|
||||||
|
from esphome import yaml_util
|
||||||
|
from esphome.core import CORE
|
||||||
from esphome.dashboard import web_server
|
from esphome.dashboard import web_server
|
||||||
from esphome.dashboard.const import DashboardEvent
|
from esphome.dashboard.const import DashboardEvent
|
||||||
from esphome.dashboard.core import DASHBOARD
|
from esphome.dashboard.core import DASHBOARD
|
||||||
@@ -1302,3 +1305,71 @@ async def test_dashboard_subscriber_refresh_event(
|
|||||||
|
|
||||||
# Give it a moment to clean up
|
# Give it a moment to clean up
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dashboard_yaml_loading_with_packages_and_secrets(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Test dashboard YAML loading with packages referencing secrets.
|
||||||
|
|
||||||
|
This is a regression test for issue #11280 where binary download failed
|
||||||
|
when using packages with secrets after the Path migration in 2025.10.0.
|
||||||
|
|
||||||
|
This test verifies that CORE.config_path initialization in the dashboard
|
||||||
|
allows yaml_util.load_yaml() to correctly resolve secrets from packages.
|
||||||
|
"""
|
||||||
|
# Create test directory structure with secrets and packages
|
||||||
|
config_dir = tmp_path / "config"
|
||||||
|
config_dir.mkdir()
|
||||||
|
|
||||||
|
# Create secrets.yaml with obviously fake test values
|
||||||
|
secrets_file = config_dir / "secrets.yaml"
|
||||||
|
secrets_file.write_text(
|
||||||
|
"wifi_ssid: TEST-DUMMY-SSID\n"
|
||||||
|
"wifi_password: not-a-real-password-just-for-testing\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create package file that uses secrets
|
||||||
|
package_file = config_dir / "common.yaml"
|
||||||
|
package_file.write_text(
|
||||||
|
"wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create main device config that includes the package
|
||||||
|
device_config = config_dir / "test-download-secrets.yaml"
|
||||||
|
device_config.write_text(
|
||||||
|
"esphome:\n name: test-download-secrets\n platform: ESP32\n board: esp32dev\n\n"
|
||||||
|
"packages:\n common: !include common.yaml\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize DASHBOARD settings with our test config directory
|
||||||
|
# This is what sets CORE.config_path - the critical code path for the bug
|
||||||
|
args = Namespace(
|
||||||
|
configuration=str(config_dir),
|
||||||
|
password=None,
|
||||||
|
username=None,
|
||||||
|
ha_addon=False,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
DASHBOARD.settings.parse_args(args)
|
||||||
|
|
||||||
|
# With the fix: CORE.config_path should be config_dir / "___DASHBOARD_SENTINEL___.yaml"
|
||||||
|
# so CORE.config_path.parent would be config_dir
|
||||||
|
# Without the fix: CORE.config_path is config_dir / "." which normalizes to config_dir
|
||||||
|
# so CORE.config_path.parent would be tmp_path (the parent of config_dir)
|
||||||
|
|
||||||
|
# The fix ensures CORE.config_path.parent points to config_dir
|
||||||
|
assert CORE.config_path.parent == config_dir.resolve(), (
|
||||||
|
f"CORE.config_path.parent should point to config_dir. "
|
||||||
|
f"Got {CORE.config_path.parent}, expected {config_dir.resolve()}. "
|
||||||
|
f"CORE.config_path is {CORE.config_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now load the YAML with packages that reference secrets
|
||||||
|
# This is where the bug would manifest - yaml_util.load_yaml would fail
|
||||||
|
# to find secrets.yaml because CORE.config_path.parent pointed to the wrong place
|
||||||
|
config = yaml_util.load_yaml(device_config)
|
||||||
|
# If we get here, secret resolution worked!
|
||||||
|
assert "esphome" in config
|
||||||
|
assert config["esphome"]["name"] == "test-download-secrets"
|
||||||
|
Reference in New Issue
Block a user