From 7767028a14d1a770020a9fe4c6d8370964f637bb Mon Sep 17 00:00:00 2001 From: SchaafAlexander <86607129+SchaafAlexander@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:55:38 +0100 Subject: [PATCH] fix(config): keep dict entries with "/" in keys during config migration The automatic field migration recursed into every dict and addressed each leaf by a "/"-joined path. Keys that themselves contain "/" - e.g. the config paths used as keys of adapter.homeassistant.config_entity_ids - were split into path elements. set_nested_value() then created a "devices" entry with an empty string default and failed further down, so every version migration replaced the mapping by {"devices": ""}. Migrate a dict whose keys contain "/" as one value instead of per key. Signed-off-by: SchaafAlexander <86607129+SchaafAlexander@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- src/akkudoktoreos/config/configmigrate.py | 10 ++++++- tests/test_configmigrate.py | 35 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/akkudoktoreos/config/configmigrate.py b/src/akkudoktoreos/config/configmigrate.py index aafc2f05..5249ee8b 100644 --- a/src/akkudoktoreos/config/configmigrate.py +++ b/src/akkudoktoreos/config/configmigrate.py @@ -497,7 +497,15 @@ def _migrate_matching_fields( if full_path in migrated_source_paths: continue - if isinstance(value, dict): + if isinstance(value, dict) and any("/" in str(k) for k in value): + # Keys containing "/" (e.g. config paths used as mapping keys) can not be + # addressed by a "/"-separated path - migrate the whole dict as one value. + try: + target_model.set_nested_value(full_path, value) + count += 1 + except Exception: + skipped_paths.append(full_path) + elif isinstance(value, dict): count += _migrate_matching_fields( value, target_model, migrated_source_paths, skipped_paths, full_path ) diff --git a/tests/test_configmigrate.py b/tests/test_configmigrate.py index 7cbe4248..4cc7eac1 100644 --- a/tests/test_configmigrate.py +++ b/tests/test_configmigrate.py @@ -264,3 +264,38 @@ class TestDeviceMigration: assert migrated.devices.batteries is not None assert migrated.devices.batteries["house"].levelized_cost_of_storage_amt_kwh == 0.123 assert raw == original + + +class TestSlashKeyedDictMigration: + """Dict entries whose keys contain '/' must survive migration unchanged.""" + + ENTITY_IDS = { + "devices/electric_vehicles/ev11/max_soc_percentage": "sensor.ev_max_soc", + "devices/electric_vehicles/ev11/min_soc_deadline_datetime": "sensor.ev_departure", + "devices/electric_vehicles/ev11/min_soc_percentage": "sensor.ev_target_soc", + } + + def test_migrate_config_data_keeps_slash_keys(self): + raw = {"adapter": {"homeassistant": {"config_entity_ids": dict(self.ENTITY_IDS)}}} + migrated = migrate_config_data(raw) + assert migrated.adapter.homeassistant.config_entity_ids == self.ENTITY_IDS + assert not any(p.startswith("adapter/homeassistant") for p in configmigrate.skipped_paths) + + def test_migrate_config_file_keeps_slash_keys(self, config_default_dirs): + config_default_dir_user, _, _, _ = config_default_dirs + config_file = config_default_dir_user.joinpath(ConfigEOS.CONFIG_FILE_NAME) + config_file.write_text( + json.dumps( + { + "general": {"version": "0.0.0-old"}, + "adapter": {"homeassistant": {"config_entity_ids": self.ENTITY_IDS}}, + } + ), + encoding="utf-8", + ) + + assert configmigrate.migrate_config_file(config_file, config_file.with_suffix(".bak")) + + migrated = json.loads(config_file.read_text(encoding="utf-8")) + assert migrated["general"]["version"] == __version__ + assert migrated["adapter"]["homeassistant"]["config_entity_ids"] == self.ENTITY_IDS