1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-18 01:33:51 +01:00

Merge pull request #11219 from esphome/bump-2025.10.0b3

2025.10.0b3
This commit is contained in:
Jesse Hills
2025-10-14 16:35:13 +13:00
committed by GitHub
9 changed files with 129 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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