mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 14:19:03 +00:00
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 = 2026.5.2
|
||||
PROJECT_NUMBER = 2026.5.3
|
||||
|
||||
# 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
|
||||
|
||||
@@ -62,6 +62,26 @@ MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29
|
||||
MODEL_CHARACTERISTIC_UUID = 0x2A24
|
||||
FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26
|
||||
|
||||
# Suffix of the Bluetooth Base UUID used to expand 16/32 bit UUIDs to 128 bit.
|
||||
_BASE_UUID_SUFFIX = "-0000-1000-8000-00805F9B34FB"
|
||||
|
||||
|
||||
def uuid_is(uuid: int | str, uuid16: int) -> bool:
|
||||
"""Return True if a validated UUID refers to the given 16-bit short UUID.
|
||||
|
||||
A service/characteristic UUID may be an ``int`` (from ``cv.hex_uint32_t``) or an
|
||||
uppercase string in 16, 32 or 128 bit form (from ``bt_uuid``), so every
|
||||
representation of the same UUID must be considered equivalent.
|
||||
"""
|
||||
if isinstance(uuid, int):
|
||||
return uuid == uuid16
|
||||
return uuid.upper() in (
|
||||
f"{uuid16:04X}",
|
||||
f"{uuid16:08X}",
|
||||
f"{uuid16:08X}{_BASE_UUID_SUFFIX}",
|
||||
)
|
||||
|
||||
|
||||
# Core key to store the global configuration
|
||||
KEY_NOTIFY_REQUIRED = "notify_required"
|
||||
KEY_SET_VALUE = "set_value"
|
||||
@@ -195,7 +215,7 @@ def create_description_cud(char_config):
|
||||
return char_config
|
||||
# If the config displays a description, there cannot be a descriptor with the CUD UUID
|
||||
for desc in char_config[CONF_DESCRIPTORS]:
|
||||
if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID:
|
||||
if uuid_is(desc[CONF_UUID], CUD_DESCRIPTOR_UUID):
|
||||
raise cv.Invalid(
|
||||
f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present"
|
||||
)
|
||||
@@ -218,7 +238,7 @@ def create_notify_cccd(char_config):
|
||||
return char_config
|
||||
# If the CCCD descriptor is already present, return the config
|
||||
for desc in char_config[CONF_DESCRIPTORS]:
|
||||
if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID:
|
||||
if uuid_is(desc[CONF_UUID], CCCD_DESCRIPTOR_UUID):
|
||||
# Check if the WRITE property is set
|
||||
if not desc[CONF_WRITE]:
|
||||
raise cv.Invalid(
|
||||
@@ -244,7 +264,7 @@ def create_device_information_service(config):
|
||||
# If there is already a device information service,
|
||||
# there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties
|
||||
for service in config[CONF_SERVICES]:
|
||||
if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
|
||||
if uuid_is(service[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
|
||||
if (
|
||||
CONF_MODEL in config
|
||||
or CONF_MANUFACTURER in config
|
||||
@@ -592,7 +612,7 @@ async def to_code(config):
|
||||
)
|
||||
for char_conf in service_config[CONF_CHARACTERISTICS]:
|
||||
await to_code_characteristic(service_var, char_conf)
|
||||
if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
|
||||
if uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
|
||||
cg.add(var.set_device_information_service(service_var))
|
||||
else:
|
||||
cg.add(var.enqueue_start_service(service_var))
|
||||
|
||||
@@ -7,6 +7,7 @@ static const char *const TAG = "remote.rc5";
|
||||
|
||||
static constexpr uint32_t BIT_TIME_US = 889;
|
||||
static constexpr uint8_t NBITS = 14;
|
||||
static constexpr uint8_t NHALFBITS = NBITS * 2;
|
||||
|
||||
void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) {
|
||||
static bool toggle = false;
|
||||
@@ -35,52 +36,63 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) {
|
||||
}
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
optional<RC5Data> RC5Protocol::decode(RemoteReceiveData src) {
|
||||
RC5Data out{
|
||||
.address = 0,
|
||||
.command = 0,
|
||||
};
|
||||
uint8_t field_bit;
|
||||
|
||||
if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) {
|
||||
field_bit = 1;
|
||||
} else if (src.expect_space(2 * BIT_TIME_US)) {
|
||||
field_bit = 0;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!(((src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US)) ||
|
||||
(src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) &&
|
||||
(((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) &&
|
||||
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) ||
|
||||
((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) &&
|
||||
(src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US)))))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
uint32_t out_data = 0;
|
||||
for (int bit = NBITS - 4; bit >= 1; bit--) {
|
||||
if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) &&
|
||||
(src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
|
||||
out_data |= 0 << bit;
|
||||
} else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) &&
|
||||
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
|
||||
out_data |= 1 << bit;
|
||||
// Expand the runs into half-bit levels (true = mark). Each run is exactly one
|
||||
// half-bit (BIT_TIME_US) or two (2 * BIT_TIME_US); stop at anything else.
|
||||
//
|
||||
// halfbits[0] is reserved for the leading half-bit, which is always dropped --
|
||||
// S1 is 1, so its first half sits at the idle level (at either polarity) and
|
||||
// merges into the pre-frame idle. Captured half-bits start at index 1.
|
||||
bool halfbits[NHALFBITS + 2];
|
||||
uint8_t n = 1;
|
||||
for (uint32_t i = 0; n <= NHALFBITS && src.is_valid(i); i++) {
|
||||
if (src.peek_mark(BIT_TIME_US, i)) {
|
||||
halfbits[n++] = true;
|
||||
} else if (src.peek_space(BIT_TIME_US, i)) {
|
||||
halfbits[n++] = false;
|
||||
} else if (src.peek_mark(2 * BIT_TIME_US, i)) {
|
||||
halfbits[n++] = true;
|
||||
halfbits[n++] = true;
|
||||
} else if (src.peek_space(2 * BIT_TIME_US, i)) {
|
||||
halfbits[n++] = false;
|
||||
halfbits[n++] = false;
|
||||
} else {
|
||||
return {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
|
||||
out_data |= 0;
|
||||
} else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
|
||||
out_data |= 1;
|
||||
|
||||
// Expect a full frame once the leading half is restored: 27 captured halves
|
||||
// (n == 28) or 26 when the final bit also ends on idle and its trailing half
|
||||
// is dropped too (n == 27). A dropped edge half is the inverse of its partner
|
||||
// (a Manchester bit always transitions mid-bit), so reconstruct the leading
|
||||
// half (always) and the trailing half (only when it was dropped).
|
||||
if (n != NHALFBITS && n != NHALFBITS - 1) {
|
||||
return {};
|
||||
}
|
||||
halfbits[0] = !halfbits[1];
|
||||
if (n == NHALFBITS - 1) {
|
||||
halfbits[n] = !halfbits[n - 1];
|
||||
}
|
||||
|
||||
out.command = (uint8_t) (out_data & 0x3F) + (1 - field_bit) * 64u;
|
||||
out.address = (out_data >> 6) & 0x1F;
|
||||
return out;
|
||||
const bool carrier = halfbits[1];
|
||||
uint16_t bits = 0;
|
||||
for (uint8_t i = 0; i < NBITS; i++) {
|
||||
const bool first = halfbits[2 * i];
|
||||
const bool second = halfbits[2 * i + 1];
|
||||
if (first == second) {
|
||||
return {}; // no midpoint transition -> not a valid Manchester bit
|
||||
}
|
||||
bits = (bits << 1) | (second == carrier ? 1 : 0);
|
||||
}
|
||||
|
||||
const bool field_bit = bits & (1 << 12); // S2: the inverted 7th command bit
|
||||
return RC5Data{
|
||||
.address = static_cast<uint8_t>((bits >> 6) & 0x1F),
|
||||
.command = static_cast<uint8_t>((bits & 0x3F) | (field_bit ? 0 : 0x40)),
|
||||
};
|
||||
}
|
||||
|
||||
void RC5Protocol::dump(const RC5Data &data) {
|
||||
ESP_LOGI(TAG, "Received RC5: address=0x%02X, command=0x%02X", data.address, data.command);
|
||||
}
|
||||
|
||||
@@ -402,18 +402,21 @@ def _generate_lwipopts_h() -> None:
|
||||
in the build directory, and a pre-build script injects this directory
|
||||
into the compiler include path before the framework's own include dir.
|
||||
"""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from jinja2 import Environment
|
||||
|
||||
lwip_defines = CORE.data[KEY_RP2040].get(KEY_LWIP_OPTS)
|
||||
if not lwip_defines:
|
||||
return
|
||||
|
||||
template_dir = Path(__file__).parent
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader(str(template_dir)),
|
||||
keep_trailing_newline=True,
|
||||
# Read the template via pathlib and render from a string rather than using
|
||||
# FileSystemLoader. jinja2's loader joins the search path with posixpath, which
|
||||
# breaks on Windows extended-length paths (\\?\C:\...) where forward slashes are
|
||||
# not accepted, causing a spurious TemplateNotFound (see issue #16732).
|
||||
template_text = (Path(__file__).parent / "lwipopts.h.jinja").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
template = jinja_env.get_template("lwipopts.h.jinja")
|
||||
jinja_env = Environment(keep_trailing_newline=True)
|
||||
template = jinja_env.from_string(template_text)
|
||||
content = template.render(**lwip_defines)
|
||||
|
||||
lwip_dir = CORE.relative_build_path("lwip_override")
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.5.2"
|
||||
__version__ = "2026.5.3"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.const import (
|
||||
KEY_TARGET_PLATFORM,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.types import CoreType
|
||||
|
||||
@@ -101,6 +101,7 @@ class StorageJSON:
|
||||
core_platform: str | None = None,
|
||||
toolchain: str | None = None,
|
||||
area: str | None = None,
|
||||
framework_version: str | None = None,
|
||||
) -> None:
|
||||
# Version of the storage JSON schema
|
||||
assert storage_version is None or isinstance(storage_version, int)
|
||||
@@ -141,6 +142,8 @@ class StorageJSON:
|
||||
self.toolchain = toolchain
|
||||
# The area of the node
|
||||
self.area = area
|
||||
# The framework version the build used (for esp32, the resolved ESP-IDF version)
|
||||
self.framework_version = framework_version
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
@@ -162,6 +165,7 @@ class StorageJSON:
|
||||
"core_platform": self.core_platform,
|
||||
"toolchain": self.toolchain,
|
||||
"area": self.area,
|
||||
"framework_version": self.framework_version,
|
||||
}
|
||||
|
||||
def to_json(self):
|
||||
@@ -173,10 +177,12 @@ class StorageJSON:
|
||||
@staticmethod
|
||||
def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON:
|
||||
hardware = esph.target_platform.upper()
|
||||
framework_version: str | None = None
|
||||
if esph.is_esp32:
|
||||
from esphome.components import esp32
|
||||
|
||||
hardware = esp32.get_esp32_variant(esph)
|
||||
framework_version = str(esp32.idf_version())
|
||||
return StorageJSON(
|
||||
storage_version=1,
|
||||
name=esph.name,
|
||||
@@ -200,6 +206,7 @@ class StorageJSON:
|
||||
core_platform=esph.target_platform,
|
||||
toolchain=esph.toolchain.value if esph.toolchain is not None else None,
|
||||
area=esph.area,
|
||||
framework_version=framework_version,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -249,6 +256,7 @@ class StorageJSON:
|
||||
core_platform = storage.get("core_platform")
|
||||
toolchain = storage.get("toolchain")
|
||||
area = storage.get("area")
|
||||
framework_version = storage.get("framework_version")
|
||||
return StorageJSON(
|
||||
storage_version,
|
||||
name,
|
||||
@@ -268,6 +276,7 @@ class StorageJSON:
|
||||
core_platform,
|
||||
toolchain,
|
||||
area,
|
||||
framework_version,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -311,10 +320,24 @@ class StorageJSON:
|
||||
# esp32.get_esp32_variant(). target_platform on disk is the variant
|
||||
# (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32").
|
||||
if target_platform == const.PLATFORM_ESP32:
|
||||
from esphome.components.esp32.const import KEY_ESP32
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
from esphome.const import KEY_VARIANT
|
||||
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform}
|
||||
esp32_data = {KEY_VARIANT: self.target_platform}
|
||||
if self.framework_version:
|
||||
import esphome.config_validation as cv
|
||||
|
||||
try:
|
||||
esp32_data[KEY_IDF_VERSION] = cv.Version.parse(
|
||||
self.framework_version
|
||||
)
|
||||
except ValueError as err:
|
||||
raise EsphomeError(
|
||||
f"Could not parse the framework version "
|
||||
f"{self.framework_version!r} from {storage_path()}. "
|
||||
f"Please clean the build files and recompile."
|
||||
) from err
|
||||
CORE.data[KEY_ESP32] = esp32_data
|
||||
|
||||
def __eq__(self, o) -> bool:
|
||||
return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict()
|
||||
|
||||
@@ -93,9 +93,12 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
|
||||
``src_version`` differs, ``build_path`` differs, the build
|
||||
``toolchain`` differs (e.g. switching between the PlatformIO and
|
||||
native ESP-IDF toolchains, which produce incompatible build trees),
|
||||
or a previously loaded integration was removed in *new*. Adding
|
||||
integrations or changing unrelated fields (friendly name, esphome
|
||||
version, etc.) does not trigger a clean.
|
||||
the ``framework`` or ``framework_version`` differs (e.g. switching
|
||||
arduino <-> esp-idf, or bumping the ESP-IDF version, which also
|
||||
produce incompatible build trees), or a previously loaded
|
||||
integration was removed in *new*. Adding integrations or changing
|
||||
unrelated fields (friendly name, esphome version, etc.) does not
|
||||
trigger a clean.
|
||||
|
||||
Used by esphome-device-builder (esphome/device-builder) to gate
|
||||
its remote-build artifact materialiser so a local → remote → local
|
||||
@@ -113,6 +116,10 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
|
||||
return True
|
||||
if old.toolchain != new.toolchain:
|
||||
return True
|
||||
if old.framework != new.framework:
|
||||
return True
|
||||
if old.framework_version != new.framework_version:
|
||||
return True
|
||||
# Check if any components have been removed
|
||||
return bool(old.loaded_integrations - new.loaded_integrations)
|
||||
|
||||
|
||||
0
tests/component_tests/esp32_ble_server/__init__.py
Normal file
0
tests/component_tests/esp32_ble_server/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Tests for esp32_ble_server configuration helpers."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32_ble_server import (
|
||||
CCCD_DESCRIPTOR_UUID,
|
||||
CUD_DESCRIPTOR_UUID,
|
||||
DEVICE_INFORMATION_SERVICE_UUID,
|
||||
uuid_is,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uuid",
|
||||
[
|
||||
DEVICE_INFORMATION_SERVICE_UUID, # int form (cv.hex_uint32_t)
|
||||
"180A", # 16 bit short form (bt_uuid)
|
||||
"180a", # lowercase is normalized by bt_uuid but guard anyway
|
||||
"0000180A", # 32 bit form
|
||||
"0000180A-0000-1000-8000-00805F9B34FB", # full 128 bit form
|
||||
],
|
||||
)
|
||||
def test_uuid_is_matches_all_representations(uuid) -> None:
|
||||
"""All representations of the same 16 bit UUID must compare equal."""
|
||||
assert uuid_is(uuid, DEVICE_INFORMATION_SERVICE_UUID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uuid",
|
||||
[
|
||||
0x1818, # Cycling Power Service (different int)
|
||||
"1818", # different 16 bit short form
|
||||
"0000180B", # adjacent UUID
|
||||
"0000180A-0000-1000-8000-00805F9B34FC", # wrong base UUID suffix
|
||||
],
|
||||
)
|
||||
def test_uuid_is_rejects_other_uuids(uuid) -> None:
|
||||
"""A different UUID must not be mistaken for the device information service."""
|
||||
assert not uuid_is(uuid, DEVICE_INFORMATION_SERVICE_UUID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uuid16", [CUD_DESCRIPTOR_UUID, CCCD_DESCRIPTOR_UUID])
|
||||
def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
|
||||
"""Reserved descriptor UUIDs match whether given as int or short string."""
|
||||
assert uuid_is(uuid16, uuid16)
|
||||
assert uuid_is(f"{uuid16:04X}", uuid16)
|
||||
assert uuid_is(f"{uuid16:08X}", uuid16)
|
||||
@@ -56,3 +56,12 @@ def test_get_esphome_esp_idf_paths_no_override():
|
||||
) as mock_install:
|
||||
toolchain._get_esphome_esp_idf_paths("5.5.4")
|
||||
mock_install.assert_called_once_with("5.5.4", source_url=None)
|
||||
|
||||
|
||||
def test_get_core_framework_version_from_core_data():
|
||||
"""The version is read from CORE.data when validation populated it."""
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
import esphome.config_validation as cv
|
||||
|
||||
CORE.data = {KEY_ESP32: {KEY_IDF_VERSION: cv.Version(5, 5, 4)}}
|
||||
assert toolchain._get_core_framework_version() == "5.5.4"
|
||||
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import storage_json
|
||||
from esphome import config_validation as cv, storage_json
|
||||
from esphome.const import CONF_DISABLED, CONF_MDNS, Toolchain
|
||||
from esphome.core import CORE
|
||||
|
||||
@@ -206,6 +206,7 @@ def test_storage_json_as_dict() -> None:
|
||||
framework="arduino",
|
||||
core_platform="esp32",
|
||||
area="Living Room",
|
||||
framework_version="5.3.1",
|
||||
)
|
||||
|
||||
result = storage.as_dict()
|
||||
@@ -235,6 +236,7 @@ def test_storage_json_as_dict() -> None:
|
||||
assert result["framework"] == "arduino"
|
||||
assert result["core_platform"] == "esp32"
|
||||
assert result["area"] == "Living Room"
|
||||
assert result["framework_version"] == "5.3.1"
|
||||
|
||||
|
||||
def test_storage_json_to_json() -> None:
|
||||
@@ -313,8 +315,12 @@ def test_storage_json_from_esphome_core(setup_core: Path) -> None:
|
||||
mock_core.toolchain = Toolchain.ESP_IDF
|
||||
mock_core.area = "Living Room"
|
||||
|
||||
with patch("esphome.components.esp32.get_esp32_variant") as mock_variant:
|
||||
with (
|
||||
patch("esphome.components.esp32.get_esp32_variant") as mock_variant,
|
||||
patch("esphome.components.esp32.idf_version") as mock_idf_version,
|
||||
):
|
||||
mock_variant.return_value = "ESP32-C3"
|
||||
mock_idf_version.return_value = cv.Version(5, 3, 1)
|
||||
|
||||
result = storage_json.StorageJSON.from_esphome_core(mock_core, old=None)
|
||||
|
||||
@@ -333,6 +339,7 @@ def test_storage_json_from_esphome_core(setup_core: Path) -> None:
|
||||
assert result.core_platform == "esp32"
|
||||
assert result.toolchain == "esp-idf"
|
||||
assert result.area == "Living Room"
|
||||
assert result.framework_version == "5.3.1"
|
||||
|
||||
|
||||
def test_storage_json_from_esphome_core_mdns_enabled(setup_core: Path) -> None:
|
||||
@@ -545,6 +552,51 @@ def test_storage_json_apply_to_core_ignores_unknown_toolchain(
|
||||
assert CORE.toolchain is None
|
||||
|
||||
|
||||
def test_storage_json_framework_version_round_trip(setup_core: Path) -> None:
|
||||
"""Sidecar framework_version restores CORE.data[esp32][idf_version]."""
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
|
||||
storage = _make_storage_with_toolchain("esp-idf")
|
||||
storage.framework_version = "5.3.1"
|
||||
path = setup_core / "storage.json"
|
||||
path.write_text(storage.to_json())
|
||||
|
||||
assert json.loads(path.read_text())["framework_version"] == "5.3.1"
|
||||
|
||||
loaded = storage_json.StorageJSON.load(path)
|
||||
assert loaded is not None
|
||||
assert loaded.framework_version == "5.3.1"
|
||||
|
||||
loaded.apply_to_core()
|
||||
assert CORE.data[KEY_ESP32][KEY_IDF_VERSION] == cv.Version(5, 3, 1)
|
||||
|
||||
|
||||
def test_storage_json_apply_to_core_without_framework_version(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""Older sidecars lacking framework_version don't populate idf_version."""
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
|
||||
loaded = _make_storage_with_toolchain("esp-idf")
|
||||
assert loaded.framework_version is None
|
||||
|
||||
loaded.apply_to_core()
|
||||
assert KEY_IDF_VERSION not in CORE.data[KEY_ESP32]
|
||||
|
||||
|
||||
def test_storage_json_apply_to_core_raises_on_invalid_framework_version(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""A malformed version string fails with an actionable error at parse time."""
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
loaded = _make_storage_with_toolchain("esp-idf")
|
||||
loaded.framework_version = "not-a-version"
|
||||
|
||||
with pytest.raises(EsphomeError, match="clean the build"):
|
||||
loaded.apply_to_core()
|
||||
|
||||
|
||||
def test_esphome_storage_json_as_dict() -> None:
|
||||
"""Test EsphomeStorageJSON.as_dict returns correct dictionary."""
|
||||
storage = storage_json.EsphomeStorageJSON(
|
||||
|
||||
@@ -76,6 +76,7 @@ def create_storage() -> Callable[..., StorageJSON]:
|
||||
framework=kwargs.get("framework", "arduino"),
|
||||
core_platform=kwargs.get("core_platform", "esp32"),
|
||||
toolchain=kwargs.get("toolchain", "platformio"),
|
||||
framework_version=kwargs.get("framework_version"),
|
||||
)
|
||||
|
||||
return _create
|
||||
@@ -121,6 +122,32 @@ def test_storage_should_clean_when_toolchain_changes(
|
||||
assert storage_should_clean(old, new) is True
|
||||
|
||||
|
||||
def test_storage_should_clean_when_framework_changes(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
) -> None:
|
||||
"""Test that clean is triggered when the framework changes.
|
||||
|
||||
Switching between arduino and esp-idf produces incompatible build trees
|
||||
even on the same toolchain, so the build must be wiped.
|
||||
"""
|
||||
old = create_storage(loaded_integrations=["api", "wifi"], framework="arduino")
|
||||
new = create_storage(loaded_integrations=["api", "wifi"], framework="esp-idf")
|
||||
assert storage_should_clean(old, new) is True
|
||||
|
||||
|
||||
def test_storage_should_clean_when_framework_version_changes(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
) -> None:
|
||||
"""Test that clean is triggered when the framework version changes.
|
||||
|
||||
A different framework/ESP-IDF version compiles against a different SDK, so
|
||||
the stale build tree must be wiped.
|
||||
"""
|
||||
old = create_storage(loaded_integrations=["api", "wifi"], framework_version="5.3.1")
|
||||
new = create_storage(loaded_integrations=["api", "wifi"], framework_version="5.4.0")
|
||||
assert storage_should_clean(old, new) is True
|
||||
|
||||
|
||||
def test_storage_should_clean_when_component_removed(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user