[core] Fix area saved as null in storage.json (#17219)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
Franck Nijhof
2026-06-29 20:30:24 +12:00
committed by Jesse Hills
co-authored by Jonathan Swoboda
parent f78cbf9200
commit 84d1c34c28
2 changed files with 37 additions and 4 deletions
+11 -1
View File
@@ -407,6 +407,17 @@ def preload_core_config(config, result) -> str:
CORE.name = conf[CONF_NAME]
CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME)
# Record the node's area name now (substitutions are already resolved at this
# point). storage.json is written before to_code() runs, so deferring this to
# to_code() left the area as null in storage.json. The value here is the raw
# post-substitution form (a plain string or a {name: ...} mapping). Assign
# unconditionally (like friendly_name) so a config without an area never
# inherits a stale value from a previous load in a long-running process, and
# use .get() so a malformed mapping surfaces later as a proper validation
# error rather than a KeyError here. to_code() sets it again from the
# validated config, which yields the same name.
area = conf.get(CONF_AREA)
CORE.area = area.get(CONF_NAME) if isinstance(area, dict) else area
CORE.data[KEY_CORE] = {}
if CONF_BUILD_PATH not in conf:
@@ -760,7 +771,6 @@ async def to_code(config: ConfigType) -> None:
# Process areas
all_areas: list[dict[str, str | core.ID]] = []
if CONF_AREA in config:
CORE.area = config[CONF_AREA][CONF_NAME]
all_areas.append(config[CONF_AREA])
all_areas.extend(config[CONF_AREAS])
+26 -3
View File
@@ -152,15 +152,21 @@ def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None:
("multiple_areas_devices.yaml", "Main Area"),
],
)
async def test_to_code_records_core_area(
async def test_core_area_recorded_at_config_load(
yaml_file: Callable[[str], Path],
fixture: str,
expected_area: str,
) -> None:
"""``to_code`` records the node's area name on CORE for StorageJSON."""
"""The node's area name is recorded on CORE for StorageJSON.
It must be set during config load (preload_core_config), not deferred to
to_code(): storage.json is written before to_code() runs, so a late
assignment left the area as null in storage.json (regression #17218).
"""
result = load_config_from_fixture(yaml_file, fixture, FIXTURES_DIR)
assert result is not None
assert CORE.area is None
# Recorded already at config-load time, before any code generation.
assert CORE.area == expected_area
with patch("esphome.core.config.cg") as mock_cg:
mock_cg.RawStatement.side_effect = lambda *args, **kwargs: MagicMock()
@@ -170,6 +176,23 @@ async def test_to_code_records_core_area(
assert CORE.area == expected_area
def test_config_load_without_area_clears_stale_core_area(
yaml_file: Callable[[str], Path],
) -> None:
"""A config without an area must not inherit a stale CORE.area.
preload_core_config assigns CORE.area unconditionally, so the area from a
previous load in a long-running process cannot leak into a config that
omits it.
"""
CORE.area = "Stale Area From Previous Load"
result = load_config_from_fixture(
yaml_file, "device_without_area.yaml", FIXTURES_DIR
)
assert result is not None
assert CORE.area is None
def test_legacy_string_area(
yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture
) -> None: