From 69df07ddcf90bb7ccec9e80cd457c3b4e41a757b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:58:30 +1300 Subject: [PATCH 1/5] [media_player.speaker] Dynamic auto load (#11084) Co-authored-by: J. Nick Koston --- esphome/components/psram/__init__.py | 2 ++ .../speaker/media_player/__init__.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 6b85e7f720..8e4f9d7eac 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -63,6 +63,8 @@ SPIRAM_SPEEDS = { def supported() -> bool: + if not CORE.is_esp32: + return False variant = get_esp32_variant() return variant in SPIRAM_MODES diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 69ea0a53c6..7537a61e4e 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, speaker +from esphome.components import audio, esp32, media_player, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -26,10 +26,21 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["audio", "psram"] + +def AUTO_LOAD(config: ConfigType) -> list[str]: + load = ["audio"] + if ( + not config + or config.get(CONF_TASK_STACK_IN_PSRAM) + or config.get(CONF_CODEC_SUPPORT_ENABLED) + ): + return load + ["psram"] + return load + CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -279,7 +290,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean, + cv.Optional( + CONF_CODEC_SUPPORT_ENABLED, default=psram.supported() + ): cv.boolean, cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, From 8627b56e36a3b618a31d5e0e42e075df36316b83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:34:12 -1000 Subject: [PATCH 2/5] Bump esphome-dashboard from 20251009.0 to 20251013.0 (#11212) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64946263ea..9937709c1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 -esphome-dashboard==20251009.0 +esphome-dashboard==20251013.0 aioesphomeapi==41.14.0 zeroconf==0.148.0 puremagic==1.30 From b666b8e2615886e6ae0cef8699cd335dc32d5170 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:12:45 -0400 Subject: [PATCH 3/5] [core] Properly clean the build dir in the HA addon (#11208) --- esphome/writer.py | 25 +++++++++++------- tests/unit_tests/test_writer.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index b5cfd9b667..8eee445cf1 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -15,6 +15,8 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + get_str_env, + is_ha_addon, read_file, walk_files, write_file_if_changed, @@ -338,16 +340,21 @@ def clean_build(): def clean_all(configuration: list[str]): import shutil - # Clean entire build dir - for dir in configuration: - build_dir = Path(dir) / ".esphome" - if build_dir.is_dir(): - _LOGGER.info("Cleaning %s", build_dir) - # Don't remove storage as it will cause the dashboard to regenerate all configs - for item in build_dir.iterdir(): - if item.is_file(): + data_dirs = [Path(dir) / ".esphome" for dir in configuration] + if is_ha_addon(): + data_dirs.append(Path("/data")) + if "ESPHOME_DATA_DIR" in os.environ: + data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None))) + + # Clean build dir + for dir in data_dirs: + if dir.is_dir(): + _LOGGER.info("Cleaning %s", dir) + # Don't remove storage or .json files which are needed by the dashboard + for item in dir.iterdir(): + if item.is_file() and not item.name.endswith(".json"): item.unlink() - elif item.name != "storage" and item.is_dir(): + elif item.is_dir() and item.name != "storage": shutil.rmtree(item) # Clean PlatformIO project files diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index bffd2b3881..a4490fbbc0 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -985,3 +985,49 @@ def test_clean_all_removes_non_storage_directories( # Verify logging mentions cleaning assert "Cleaning" in caplog.text assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all_preserves_json_files( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all preserves .json files.""" + # Create build directory with various files + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # Create .json files (should be preserved) + (build_dir / "config.json").write_text('{"config": "data"}') + (build_dir / "metadata.json").write_text('{"metadata": "info"}') + + # Create non-.json files (should be removed) + (build_dir / "dummy.txt").write_text("x") + (build_dir / "other.log").write_text("log content") + + # Call clean_all + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(config_dir)]) + + # Verify .esphome directory still exists + assert build_dir.exists() + + # Verify .json files are preserved + assert (build_dir / "config.json").exists() + assert (build_dir / "config.json").read_text() == '{"config": "data"}' + assert (build_dir / "metadata.json").exists() + assert (build_dir / "metadata.json").read_text() == '{"metadata": "info"}' + + # Verify non-.json files were removed + assert not (build_dir / "dummy.txt").exists() + assert not (build_dir / "other.log").exists() + + # Verify logging mentions cleaning + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text From 07504c82081366744ecb920753dd6da841da2170 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 11:23:44 -1000 Subject: [PATCH 4/5] Fix log retrieval with FQDN when mDNS is disabled (#11202) --- esphome/__main__.py | 15 +++++++----- tests/unit_tests/test_main.py | 45 ++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index be4551f6b5..8e0c475525 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -268,8 +268,10 @@ def has_ip_address() -> bool: def has_resolvable_address() -> bool: - """Check if CORE.address is resolvable (via mDNS or is an IP address).""" - return has_mdns() or has_ip_address() + """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" + # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable + # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver + return CORE.address is not None def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): @@ -578,11 +580,12 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int if has_api(): addresses_to_use: list[str] | None = None - if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)): + if port_type == "NETWORK": + # Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used + # The resolve_ip_address() function in helpers.py handles all types addresses_to_use = devices - elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup(): - # Only use MQTT IP lookup if the first condition didn't match - # (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails) + elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup(): + # Use MQTT IP lookup for MQTT/MQTTIP types addresses_to_use = mqtt_get_ip( config, args.username, args.password, args.client_id ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index e35378145a..becf911fa3 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1203,6 +1203,31 @@ def test_show_logs_api( ) +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_with_fqdn_mdns_disabled( + mock_run_logs: Mock, +) -> None: + """Test show_logs with API using FQDN when mDNS is disabled.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: True}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + devices = ["device.example.com"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + # Should use the FQDN directly, not try MQTT lookup + mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"]) + + @patch("esphome.components.api.client.run_logs") def test_show_logs_api_with_mqtt_fallback( mock_run_logs: Mock, @@ -1222,7 +1247,7 @@ def test_show_logs_api_with_mqtt_fallback( mock_mqtt_get_ip.return_value = ["192.168.1.200"] args = MockArgs(username="user", password="pass", client_id="client") - devices = ["device.local"] + devices = ["MQTTIP"] result = show_logs(CORE.config, args, devices) @@ -1487,27 +1512,31 @@ def test_mqtt_get_ip() -> None: def test_has_resolvable_address() -> None: """Test has_resolvable_address function.""" - # Test with mDNS enabled and hostname address + # Test with mDNS enabled and .local hostname address setup_core(config={}, address="esphome-device.local") assert has_resolvable_address() is True - # Test with mDNS disabled and hostname address + # Test with mDNS disabled and .local hostname address (still resolvable via DNS) setup_core( config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local" ) - assert has_resolvable_address() is False + assert has_resolvable_address() is True - # Test with IP address (mDNS doesn't matter) + # Test with mDNS disabled and regular DNS hostname (resolvable) + setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com") + assert has_resolvable_address() is True + + # Test with IP address (always resolvable, mDNS doesn't matter) setup_core(config={}, address="192.168.1.100") assert has_resolvable_address() is True - # Test with IP address and mDNS disabled + # Test with IP address and mDNS disabled (still resolvable) setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100") assert has_resolvable_address() is True - # Test with no address but mDNS enabled (can still resolve mDNS names) + # Test with no address setup_core(config={}, address=None) - assert has_resolvable_address() is True + assert has_resolvable_address() is False # Test with no address and mDNS disabled setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None) From d02ed41eb4adf2001df33b745431a04226022e9c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:38:15 +1300 Subject: [PATCH 5/5] Bump version to 2025.10.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 37ec90c851..d664ca5d63 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.10.0b2 +PROJECT_NUMBER = 2025.10.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index a086f90385..76943ff59d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.0b2" +__version__ = "2025.10.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (