Compare commits

...

9 Commits

Author SHA1 Message Date
Jonathan Swoboda
d8c23d4fc9 Merge pull request #12772 from esphome/bump-2025.12.4
2025.12.4
2025-12-31 17:42:39 -05:00
Jonathan Swoboda
e9e0712959 Bump version to 2025.12.4 2025-12-31 16:07:00 -05:00
J. Nick Koston
062840dd7b [docker] Add build-essential to fix ruamel.yaml 0.19.0 compilation (#12769)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-31 16:07:00 -05:00
J. Nick Koston
f0f01c081a [wifi] Fix ESP-IDF reporting connected before DHCP completes on reconnect (#12755) 2025-12-31 16:07:00 -05:00
Stuart Parmenter
dd855985be [hub75] Add clipping check (#12762) 2025-12-31 16:06:59 -05:00
Jonathan Swoboda
5b5cede5f9 Merge pull request #12752 from esphome/bump-2025.12.3
2025.12.3
2025-12-30 09:31:31 -05:00
Jonathan Swoboda
c737033cc4 Bump version to 2025.12.3 2025-12-30 09:22:03 -05:00
J. Nick Koston
0194bfd9ea [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-30 09:22:03 -05:00
J. Nick Koston
339399eb70 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-30 09:22:03 -05:00
10 changed files with 134 additions and 23 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.12.2
PROJECT_NUMBER = 2025.12.4
# 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

@@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*"
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14

View File

@@ -111,6 +111,9 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
return;
if (!this->get_clipping().inside(x, y))
return;
driver_->set_pixel(x, y, color.r, color.g, color.b);
App.feed_wdt();
}

View File

@@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
@@ -96,13 +96,9 @@ class LValidator:
return None
if isinstance(value, Lambda):
# Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext
from .lvcode import get_lambda_context_args
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
args = args or get_lambda_context_args()
return cg.RawExpression(
call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)

View File

@@ -1,5 +1,5 @@
import re
from typing import TYPE_CHECKING, Any
from typing import Any
import esphome.codegen as cg
from esphome.components import image
@@ -404,14 +404,9 @@ class TextValidator(LValidator):
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
# Local import to avoid circular import at module level
from .lvcode import get_lambda_context_args
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
args = args or get_lambda_context_args()
if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT):

View File

@@ -1,4 +1,5 @@
import abc
from typing import TYPE_CHECKING
from esphome import codegen as cg
from esphome.config import Config
@@ -200,6 +201,21 @@ class LvContext(LambdaContext):
return self.add(*args)
def get_lambda_context_args() -> list[tuple[SafeExpType, str]]:
"""Get automation parameters from the current lambda context if available.
When called from outside LVGL's context (e.g., from interval),
CodeContext.code_context will be None, so return empty args.
"""
if CodeContext.code_context is None:
return []
if TYPE_CHECKING:
# CodeContext base class doesn't define get_automation_parameters(),
# but LambdaContext and LvContext (the concrete implementations) do.
assert isinstance(CodeContext.code_context, LambdaContext)
return CodeContext.code_context.get_automation_parameters()
class LocalVariable(MockObj):
"""
Create a local variable and enclose the code using it within a block.

View File

@@ -483,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
s_sta_connected = false;
s_sta_connect_error = false;
s_sta_connect_not_found = false;
// Reset IP address flags - ensures we don't report connected before DHCP completes
// (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect)
this->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
this->num_ipv6_addresses_ = 0;
#endif
err = esp_wifi_connect();
if (err != ESP_OK) {

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.12.2"
__version__ = "2025.12.4"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -99,14 +99,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
if (
# ESP32 uses CMake for both Arduino and ESP-IDF frameworks
return (
old.loaded_integrations != new.loaded_integrations
or old.loaded_platforms != new.loaded_platforms
) and new.core_platform == PLATFORM_ESP32:
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return False
) and new.core_platform == PLATFORM_ESP32
def update_storage_json() -> None:

View File

@@ -9,6 +9,13 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import EsphomeError
from esphome.storage_json import StorageJSON
from esphome.writer import (
@@ -21,6 +28,7 @@ from esphome.writer import (
clean_build,
clean_cmake_cache,
storage_should_clean,
storage_should_update_cmake_cache,
update_storage_json,
write_cpp,
write_gitignore,
@@ -164,6 +172,86 @@ def test_storage_edge_case_from_empty_integrations(
assert storage_should_clean(old, new) is False
# Tests for storage_should_update_cmake_cache
@pytest.mark.parametrize("framework", ["arduino", "esp-idf"])
def test_storage_should_update_cmake_cache_when_integration_added_esp32(
create_storage: Callable[..., StorageJSON],
framework: str,
) -> None:
"""Test cmake cache update triggered when integration added on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=PLATFORM_ESP32,
framework=framework,
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_update_cmake_cache_when_platform_changed_esp32(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache update triggered when platforms change on ESP32."""
old = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
loaded_platforms={"sensor", "binary_sensor"},
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is True
def test_storage_should_not_update_cmake_cache_when_nothing_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test cmake cache not updated when nothing changes."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=PLATFORM_ESP32,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@pytest.mark.parametrize(
"core_platform",
[PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX],
)
def test_storage_should_not_update_cmake_cache_for_non_esp32(
create_storage: Callable[..., StorageJSON],
core_platform: str,
) -> None:
"""Test cmake cache not updated for non-ESP32 platforms."""
old = create_storage(
loaded_integrations=["api", "wifi"],
core_platform=core_platform,
framework="arduino",
)
new = create_storage(
loaded_integrations=["api", "wifi", "restart"],
core_platform=core_platform,
framework="arduino",
)
assert storage_should_update_cmake_cache(old, new) is False
@patch("esphome.writer.clean_build")
@patch("esphome.writer.StorageJSON")
@patch("esphome.writer.storage_path")