mirror of
https://github.com/esphome/esphome.git
synced 2025-10-19 18:23:46 +01:00
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# 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
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
@@ -268,8 +268,10 @@ def has_ip_address() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def has_resolvable_address() -> bool:
|
def has_resolvable_address() -> bool:
|
||||||
"""Check if CORE.address is resolvable (via mDNS or is an IP address)."""
|
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
||||||
return has_mdns() or has_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):
|
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():
|
if has_api():
|
||||||
addresses_to_use: list[str] | None = None
|
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
|
addresses_to_use = devices
|
||||||
elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
|
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
|
||||||
# Only use MQTT IP lookup if the first condition didn't match
|
# Use MQTT IP lookup for MQTT/MQTTIP types
|
||||||
# (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
|
|
||||||
addresses_to_use = mqtt_get_ip(
|
addresses_to_use = mqtt_get_ip(
|
||||||
config, args.username, args.password, args.client_id
|
config, args.username, args.password, args.client_id
|
||||||
)
|
)
|
||||||
|
@@ -63,6 +63,8 @@ SPIRAM_SPEEDS = {
|
|||||||
|
|
||||||
|
|
||||||
def supported() -> bool:
|
def supported() -> bool:
|
||||||
|
if not CORE.is_esp32:
|
||||||
|
return False
|
||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
return variant in SPIRAM_MODES
|
return variant in SPIRAM_MODES
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from esphome import automation, external_files
|
from esphome import automation, external_files
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
@@ -26,10 +26,21 @@ from esphome.const import (
|
|||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt
|
||||||
from esphome.core.entity_helpers import inherit_property_from
|
from esphome.core.entity_helpers import inherit_property_from
|
||||||
from esphome.external_files import download_content
|
from esphome.external_files import download_content
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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"]
|
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
||||||
DOMAIN = "media_player"
|
DOMAIN = "media_player"
|
||||||
@@ -279,7 +290,9 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
|
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
|
||||||
min=4000, max=4000000
|
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_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
|
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
||||||
|
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.10.0b2"
|
__version__ = "2025.10.0b3"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
@@ -15,6 +15,8 @@ from esphome.const import (
|
|||||||
from esphome.core import CORE, EsphomeError
|
from esphome.core import CORE, EsphomeError
|
||||||
from esphome.helpers import (
|
from esphome.helpers import (
|
||||||
copy_file_if_changed,
|
copy_file_if_changed,
|
||||||
|
get_str_env,
|
||||||
|
is_ha_addon,
|
||||||
read_file,
|
read_file,
|
||||||
walk_files,
|
walk_files,
|
||||||
write_file_if_changed,
|
write_file_if_changed,
|
||||||
@@ -338,16 +340,21 @@ def clean_build():
|
|||||||
def clean_all(configuration: list[str]):
|
def clean_all(configuration: list[str]):
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
# Clean entire build dir
|
data_dirs = [Path(dir) / ".esphome" for dir in configuration]
|
||||||
for dir in configuration:
|
if is_ha_addon():
|
||||||
build_dir = Path(dir) / ".esphome"
|
data_dirs.append(Path("/data"))
|
||||||
if build_dir.is_dir():
|
if "ESPHOME_DATA_DIR" in os.environ:
|
||||||
_LOGGER.info("Cleaning %s", build_dir)
|
data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None)))
|
||||||
# Don't remove storage as it will cause the dashboard to regenerate all configs
|
|
||||||
for item in build_dir.iterdir():
|
# Clean build dir
|
||||||
if item.is_file():
|
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()
|
item.unlink()
|
||||||
elif item.name != "storage" and item.is_dir():
|
elif item.is_dir() and item.name != "storage":
|
||||||
shutil.rmtree(item)
|
shutil.rmtree(item)
|
||||||
|
|
||||||
# Clean PlatformIO project files
|
# Clean PlatformIO project files
|
||||||
|
@@ -11,7 +11,7 @@ pyserial==3.5
|
|||||||
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||||
esptool==5.1.0
|
esptool==5.1.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20251009.0
|
esphome-dashboard==20251013.0
|
||||||
aioesphomeapi==41.14.0
|
aioesphomeapi==41.14.0
|
||||||
zeroconf==0.148.0
|
zeroconf==0.148.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
|
@@ -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")
|
@patch("esphome.components.api.client.run_logs")
|
||||||
def test_show_logs_api_with_mqtt_fallback(
|
def test_show_logs_api_with_mqtt_fallback(
|
||||||
mock_run_logs: Mock,
|
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"]
|
mock_mqtt_get_ip.return_value = ["192.168.1.200"]
|
||||||
|
|
||||||
args = MockArgs(username="user", password="pass", client_id="client")
|
args = MockArgs(username="user", password="pass", client_id="client")
|
||||||
devices = ["device.local"]
|
devices = ["MQTTIP"]
|
||||||
|
|
||||||
result = show_logs(CORE.config, args, devices)
|
result = show_logs(CORE.config, args, devices)
|
||||||
|
|
||||||
@@ -1487,27 +1512,31 @@ def test_mqtt_get_ip() -> None:
|
|||||||
def test_has_resolvable_address() -> None:
|
def test_has_resolvable_address() -> None:
|
||||||
"""Test has_resolvable_address function."""
|
"""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")
|
setup_core(config={}, address="esphome-device.local")
|
||||||
assert has_resolvable_address() is True
|
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(
|
setup_core(
|
||||||
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
|
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")
|
setup_core(config={}, address="192.168.1.100")
|
||||||
assert has_resolvable_address() is True
|
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")
|
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100")
|
||||||
assert has_resolvable_address() is True
|
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)
|
setup_core(config={}, address=None)
|
||||||
assert has_resolvable_address() is True
|
assert has_resolvable_address() is False
|
||||||
|
|
||||||
# Test with no address and mDNS disabled
|
# Test with no address and mDNS disabled
|
||||||
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None)
|
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None)
|
||||||
|
@@ -985,3 +985,49 @@ def test_clean_all_removes_non_storage_directories(
|
|||||||
# Verify logging mentions cleaning
|
# Verify logging mentions cleaning
|
||||||
assert "Cleaning" in caplog.text
|
assert "Cleaning" in caplog.text
|
||||||
assert str(build_dir) 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
|
||||||
|
Reference in New Issue
Block a user