mirror of
https://github.com/esphome/esphome.git
synced 2025-10-15 14:48:44 +00:00
Compare commits
15 Commits
esp32_ble_
...
2025.10.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a29dec6d9 | ||
![]() |
63b113d823 | ||
![]() |
0381644605 | ||
![]() |
48a557b005 | ||
![]() |
780ece73ff | ||
![]() |
d7fcf8d57b | ||
![]() |
82a3ca575f | ||
![]() |
5913da5a89 | ||
![]() |
8c13105ce1 | ||
![]() |
c3fd07f8bc | ||
![]() |
d02ed41eb4 | ||
![]() |
07504c8208 | ||
![]() |
b666b8e261 | ||
![]() |
8627b56e36 | ||
![]() |
69df07ddcf |
2
Doxyfile
2
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.0
|
||||
|
||||
# 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
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -56,50 +56,41 @@ DriverChip(
|
||||
"WAVESHARE-P4-86-PANEL",
|
||||
height=720,
|
||||
width=720,
|
||||
hsync_back_porch=80,
|
||||
hsync_back_porch=50,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=80,
|
||||
vsync_back_porch=12,
|
||||
hsync_front_porch=50,
|
||||
vsync_back_porch=20,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="46MHz",
|
||||
lane_bit_rate="1Gbps",
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="38MHz",
|
||||
lane_bit_rate="480Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=27,
|
||||
initsequence=[
|
||||
(0xB9, 0xF1, 0x12, 0x83),
|
||||
(
|
||||
0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00,
|
||||
0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
|
||||
),
|
||||
(0xB8, 0x25, 0x22, 0xF0, 0x63),
|
||||
(0xBF, 0x02, 0x11, 0x00),
|
||||
(0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80),
|
||||
(0xB2, 0x3C, 0x12, 0x30),
|
||||
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
|
||||
(0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
|
||||
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
|
||||
(0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
|
||||
(0xB4, 0x80),
|
||||
(0xB5, 0x0A, 0x0A),
|
||||
(0xB6, 0xB2, 0xB2),
|
||||
(
|
||||
0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80,
|
||||
0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48,
|
||||
0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57,
|
||||
0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
),
|
||||
(
|
||||
0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31,
|
||||
0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64,
|
||||
0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
),
|
||||
(
|
||||
0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14,
|
||||
0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13,
|
||||
0x15, 0x14, 0x15, 0x10, 0x17,
|
||||
),
|
||||
(0xB6, 0x97, 0x97),
|
||||
(0xB8, 0x26, 0x22, 0xF0, 0x13),
|
||||
(0xBA, 0x31, 0x81, 0x0F, 0xF9, 0x0E, 0x06, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37),
|
||||
(0xBC, 0x47),
|
||||
(0xBF, 0x02, 0x11, 0x00),
|
||||
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
|
||||
(0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77),
|
||||
(0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF),
|
||||
(0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09),
|
||||
(0xC8, 0x10, 0x40, 0x1E, 0x02),
|
||||
(0xCC, 0x0B),
|
||||
(0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16),
|
||||
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10),
|
||||
(0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEF, 0xFF, 0xFF, 0x01),
|
||||
(0x11, 0x00),
|
||||
(0x29, 0x00),
|
||||
],
|
||||
)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.10.0b2"
|
||||
__version__ = "2025.10.0"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
@@ -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
|
||||
|
@@ -1,3 +1,4 @@
|
||||
[build]
|
||||
command = "script/build-api-docs"
|
||||
publish = "api-docs"
|
||||
environment = { PYTHON_VERSION = "3.13" }
|
||||
|
@@ -11,8 +11,8 @@ 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
|
||||
aioesphomeapi==41.14.0
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==41.16.1
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user