diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 1fcaa7d6a30a..e76b5d3772cb 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -68,9 +68,7 @@ async def async_setup_entry( KnxYamlBinarySensor(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get( - Platform.BINARY_SENSOR - ): + if ui_config := knx_module.config_store.get_entity_configs(Platform.BINARY_SENSOR): entities.extend( KnxUiBinarySensor(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index ae29fc8fc4d2..1ed7978ba5b6 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -49,7 +49,7 @@ async def async_setup_entry( KnxYamlButton(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.BUTTON): + if ui_config := knx_module.config_store.get_entity_configs(Platform.BUTTON): entities.extend( KnxUiButton(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 543f7196fb68..37b6f0aeabd5 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -103,7 +103,7 @@ async def async_setup_entry( KnxYamlClimate(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.CLIMATE): + if ui_config := knx_module.config_store.get_entity_configs(Platform.CLIMATE): entities.extend( KnxUiClimate(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 54b3df8805e9..66bbcc850f4b 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -124,6 +124,7 @@ SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register" SERVICE_KNX_READ: Final = "read" REPAIR_ISSUE_DATA_SECURE_GROUP_KEY: Final = "data_secure_group_key_issue" +REPAIR_ISSUE_ENTITY_VALIDATION_ERROR: Final = "entity_validation_error" REPAIR_ISSUE_TELEGRAM_BACKEND_ERROR: Final = "telegram_backend_error" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 22fee8301277..9270e4e2b8ce 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -74,7 +74,7 @@ async def async_setup_entry( KnxYamlCover(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.COVER): + if ui_config := knx_module.config_store.get_entity_configs(Platform.COVER): entities.extend( KnxUiCover(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 9fa40c87b14b..e6996d570a9a 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -59,7 +59,7 @@ async def async_setup_entry( KnxYamlDate(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.DATE): + if ui_config := knx_module.config_store.get_entity_configs(Platform.DATE): entities.extend( KnxUiDate(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index f73735e635bc..0b7964cd61c3 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -60,7 +60,7 @@ async def async_setup_entry( KnxYamlDateTime(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.DATETIME): + if ui_config := knx_module.config_store.get_entity_configs(Platform.DATETIME): entities.extend( KnxUiDateTime(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/entity.py b/homeassistant/components/knx/entity.py index 187c890590ce..37571cfd9528 100644 --- a/homeassistant/components/knx/entity.py +++ b/homeassistant/components/knx/entity.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_UNIQUE_ID, - EntityCategory, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -267,7 +266,6 @@ class KnxUiEntity(_KnxEntityBase): self._attr_name = entity_config[CONF_NAME] self._attr_unique_id = unique_id - if entity_category := entity_config.get(CONF_ENTITY_CATEGORY): - self._attr_entity_category = EntityCategory(entity_category) - if device_info := entity_config.get(CONF_DEVICE_INFO): + self._attr_entity_category = entity_config[CONF_ENTITY_CATEGORY] + if device_info := entity_config[CONF_DEVICE_INFO]: self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index d288465380b2..36acb2d35fac 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -122,7 +122,7 @@ async def async_setup_entry( KnxYamlFan(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.FAN): + if ui_config := knx_module.config_store.get_entity_configs(Platform.FAN): entities.extend( KnxUiFan(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 353796ae2307..fa4455b7cd09 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -83,7 +83,7 @@ async def async_setup_entry( KnxYamlLight(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): + if ui_config := knx_module.config_store.get_entity_configs(Platform.LIGHT): entities.extend( KnxUiLight(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 039de497a55f..96e5ba7c1375 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -49,7 +49,7 @@ async def async_setup_entry( KnxYamlNotify(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.NOTIFY): + if ui_config := knx_module.config_store.get_entity_configs(Platform.NOTIFY): entities.extend( KnxUiNotify(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 0a1ed401563a..3f679243342f 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -68,7 +68,7 @@ async def async_setup_entry( KnxYamlNumber(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.NUMBER): + if ui_config := knx_module.config_store.get_entity_configs(Platform.NUMBER): entities.extend( KnxUiNumber(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/repairs.py b/homeassistant/components/knx/repairs.py index cba639cd5132..4a3b90e4becd 100644 --- a/homeassistant/components/knx/repairs.py +++ b/homeassistant/components/knx/repairs.py @@ -2,6 +2,7 @@ from collections.abc import Callable from functools import partial +import logging from typing import TYPE_CHECKING, Any, Final import voluptuous as vol @@ -9,6 +10,7 @@ from xknx.exceptions.exception import InvalidSecureConfiguration from xknx.telegram import GroupAddress, IndividualAddress, Telegram from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import issue_registry as ir, selector from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,12 +23,15 @@ from .const import ( CONF_KNX_KNXKEY_PASSWORD, DOMAIN, REPAIR_ISSUE_DATA_SECURE_GROUP_KEY, + REPAIR_ISSUE_ENTITY_VALIDATION_ERROR, REPAIR_ISSUE_TELEGRAM_BACKEND_ERROR, SIGNAL_KNX_DATA_SECURE_ISSUE_TELEGRAM, KNXConfigEntryData, ) from .storage.keyring import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file +_LOGGER = logging.getLogger(__name__) + CONF_KEYRING_FILE: Final = "knxkeys_file" @@ -43,6 +48,35 @@ async def async_create_fix_flow( raise ValueError(f"unknown repair {issue_id}") +########################### +# Entity store schema issue +########################### + + +@callback +def async_create_entity_validation_issue( + hass: HomeAssistant, platform: Platform, unique_ids: list[str] +) -> None: + """Create a repair issue for invalid entity configurations in the config store.""" + _LOGGER.error( + "Invalid KNX %s configuration in storage. These entities were not set up: %s", + platform, + ", ".join(unique_ids), + ) + ir.async_create_issue( + hass, + DOMAIN, + f"{REPAIR_ISSUE_ENTITY_VALIDATION_ERROR}_{platform}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key=REPAIR_ISSUE_ENTITY_VALIDATION_ERROR, + translation_placeholders={ + "platform": platform, + "entities": "\n".join(f"- {unique_id}" for unique_id in unique_ids), + }, + ) + + ###################### # DataSecure key issue ###################### diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 10012540c2ca..ef95fec8dffb 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -51,7 +51,7 @@ async def async_setup_entry( KnxYamlScene(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.SCENE): + if ui_config := knx_module.config_store.get_entity_configs(Platform.SCENE): entities.extend( KnxUiScene(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index b46b8295ac30..350eb23495b3 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -71,7 +71,7 @@ async def async_setup_entry( KnxYamlSelect(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.SELECT): + if ui_config := knx_module.config_store.get_entity_configs(Platform.SELECT): entities.extend( KnxUiSelect(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ba7f8b3fd9d2..f65922a99fdf 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -156,7 +156,7 @@ async def async_setup_entry( KnxYamlSensor(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.SENSOR): + if ui_config := knx_module.config_store.get_entity_configs(Platform.SENSOR): entities.extend( KnxUiSensor(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 2c8a19c54b12..2b1da7940313 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -12,8 +12,13 @@ from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN, KNX_MODULE_KEY +from ..repairs import async_create_entity_validation_issue from . import migration from .const import CONF_DATA +from .entity_store_validation import ( + EntityStoreValidationException, + validate_entity_data, +) from .expose_controller import KNXExposeStoreConfigModel, KNXExposeStoreModel from .time_server import KNXTimeServerStoreModel @@ -118,6 +123,28 @@ class KNXConfigStore: """Add platform controller.""" self._platform_controllers[platform] = controller + @callback + def get_entity_configs(self, platform: Platform) -> KNXPlatformStoreModel: + """Return validated entity configurations for a platform. + + Invalid configurations are reported as a repair issue and stay in + `self.data` so they aren't dropped from storage. + """ + validated: KNXPlatformStoreModel = {} + invalid: list[str] = [] + for unique_id, config in self.data["entities"].get(platform, {}).items(): + try: + result = validate_entity_data( + {CONF_PLATFORM: platform, CONF_DATA: config} + ) + except EntityStoreValidationException: + invalid.append(unique_id) + else: + validated[unique_id] = result[CONF_DATA] + if invalid: + async_create_entity_validation_issue(self.hass, platform, invalid) + return validated + async def create_entity( self, platform: Platform, data: dict[str, Any] ) -> str | None: diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index 23abcc612e9b..bfbc0998a619 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -408,19 +408,21 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst( probatio.Optional(CONF_GA_COLOR_TEMP): GASelector( write_required=True, dpt=ColorTempModes ), - probatio.Required( - CONF_COLOR_TEMP_MIN, default=2700 - ): selector.NumberSelector( - selector.NumberSelectorConfig( - min=1, max=10000, step=1, unit_of_measurement="K" - ) + probatio.Required(CONF_COLOR_TEMP_MIN, default=2700): AllSerializeFirst( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=10000, step=1, unit_of_measurement="K" + ) + ), + probatio.Coerce(int), ), - probatio.Required( - CONF_COLOR_TEMP_MAX, default=6000 - ): selector.NumberSelector( - selector.NumberSelectorConfig( - min=1, max=10000, step=1, unit_of_measurement="K" - ) + probatio.Required(CONF_COLOR_TEMP_MAX, default=6000): AllSerializeFirst( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=10000, step=1, unit_of_measurement="K" + ) + ), + probatio.Coerce(int), ), probatio.Optional(CONF_COLOR): GroupSelect( GroupSelectOption( diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index faec2c8182b9..d930ab5c1dc1 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -1321,6 +1321,10 @@ }, "title": "KNX Data Secure telegrams can't be decrypted" }, + "entity_validation_error": { + "description": "The stored configuration of the following KNX {platform} entities is invalid, so they were not set up:\n\n{entities}\n\nCorrect or delete them. Check the logs for details.", + "title": "Invalid KNX entity configuration" + }, "telegram_storage_error": { "description": "The configured KNX telegram storage backend failed to initialize. As a result, KNX telegrams are currently not being stored. Check the logs for details on the error and ensure your database is accessible.", "title": "KNX telegram storage error" diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 55d622b33dc7..200cc391d79a 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -65,7 +65,7 @@ async def async_setup_entry( KnxYamlSwitch(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): + if ui_config := knx_module.config_store.get_entity_configs(Platform.SWITCH): entities.extend( KnxUiSwitch(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index af0a8d37a1df..f035740aa607 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -66,7 +66,7 @@ async def async_setup_entry( KnxYamlText(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.TEXT): + if ui_config := knx_module.config_store.get_entity_configs(Platform.TEXT): entities.extend( KnxUiText(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index a3c4d6d63d63..19b962e4eb43 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -59,7 +59,7 @@ async def async_setup_entry( KnxYamlTime(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.TIME): + if ui_config := knx_module.config_store.get_entity_configs(Platform.TIME): entities.extend( KnxUiTime(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 14b45d418375..03315f8efd7e 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -72,7 +72,7 @@ async def async_setup_entry( KnxYamlWeather(knx_module, entity_config) for entity_config in yaml_platform_config ) - if ui_config := knx_module.config_store.data["entities"].get(Platform.WEATHER): + if ui_config := knx_module.config_store.get_entity_configs(Platform.WEATHER): entities.extend( KnxUiWeather(knx_module, unique_id, config) for unique_id, config in ui_config.items() diff --git a/tests/components/knx/fixtures/config_store_date.json b/tests/components/knx/fixtures/config_store_date.json index 48cbc29abde4..5d69052790cc 100644 --- a/tests/components/knx/fixtures/config_store_date.json +++ b/tests/components/knx/fixtures/config_store_date.json @@ -34,7 +34,7 @@ "passive": [] }, "respond_to_read": false, - "sync_state": false + "sync_state": true } } } diff --git a/tests/components/knx/fixtures/config_store_datetime.json b/tests/components/knx/fixtures/config_store_datetime.json index c5dbd5fc88f9..c6ff74c20208 100644 --- a/tests/components/knx/fixtures/config_store_datetime.json +++ b/tests/components/knx/fixtures/config_store_datetime.json @@ -34,7 +34,7 @@ "passive": [] }, "respond_to_read": false, - "sync_state": false + "sync_state": true } } } diff --git a/tests/components/knx/fixtures/config_store_invalid.json b/tests/components/knx/fixtures/config_store_invalid.json new file mode 100644 index 000000000000..70322ec49a58 --- /dev/null +++ b/tests/components/knx/fixtures/config_store_invalid.json @@ -0,0 +1,64 @@ +{ + "version": 2, + "minor_version": 4, + "key": "knx/config_store.json", + "data": { + "entities": { + "switch": { + "knx_es_01JWDFHP1ZG6NT62BX6ENR3MG7": { + "entity": { + "name": "valid", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "ga_switch": { + "write": "1/1/45", + "state": "1/0/45", + "passive": [] + }, + "invert": false, + "sync_state": true, + "respond_to_read": false + } + }, + "knx_es_01JWDFKBG3PYPPRQDJZ3N3PMCB": { + "entity": { + "name": "invalid group address", + "device_info": null, + "entity_category": null + }, + "knx": { + "ga_switch": { + "write": "not a group address", + "state": null, + "passive": [] + }, + "invert": false, + "sync_state": true, + "respond_to_read": false + } + } + }, + "light": { + "knx_es_01J85ZKTFHSZNG4X9DYBE592TF": { + "entity": { + "name": "missing defaults", + "device_info": null, + "entity_category": null + }, + "knx": { + "ga_switch": { + "write": "1/1/21", + "state": "1/0/21", + "passive": [] + }, + "sync_state": true + } + } + } + }, + "expose": {}, + "time_server": {} + } +} diff --git a/tests/components/knx/fixtures/config_store_time.json b/tests/components/knx/fixtures/config_store_time.json index ad0d6de2ed52..fa6c2bb8a88b 100644 --- a/tests/components/knx/fixtures/config_store_time.json +++ b/tests/components/knx/fixtures/config_store_time.json @@ -34,7 +34,7 @@ "passive": [] }, "respond_to_read": false, - "sync_state": false + "sync_state": true } } } diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py index 7edf7cc2f6ea..04f7618c8e3c 100644 --- a/tests/components/knx/test_config_store.py +++ b/tests/components/knx/test_config_store.py @@ -4,12 +4,17 @@ from typing import Any import pytest +from homeassistant.components.knx.const import ( + DOMAIN, + KNX_MODULE_KEY, + REPAIR_ISSUE_ENTITY_VALIDATION_ERROR, +) from homeassistant.components.knx.storage.config_store import ( STORAGE_KEY as KNX_CONFIG_STORAGE_KEY, ) -from homeassistant.const import Platform +from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from . import KnxEntityGenerator from .conftest import KNXTestKit @@ -605,6 +610,86 @@ async def test_delete_expose_error( ) +################## +# STORE VALIDATION +################## + +VALID_SWITCH_UID = "knx_es_01JWDFHP1ZG6NT62BX6ENR3MG7" +INVALID_SWITCH_UID = "knx_es_01JWDFKBG3PYPPRQDJZ3N3PMCB" +LIGHT_UID = "knx_es_01J85ZKTFHSZNG4X9DYBE592TF" + + +async def test_load_skips_invalid_entity_config( + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test an invalid stored config is skipped without failing its platform.""" + await knx.setup_integration( + config_store_fixture="config_store_invalid.json", state_updater=False + ) + assert entity_registry.async_get_entity_id( + Platform.SWITCH, DOMAIN, VALID_SWITCH_UID + ) + assert ( + entity_registry.async_get_entity_id(Platform.SWITCH, DOMAIN, INVALID_SWITCH_UID) + is None + ) + + issue = issue_registry.async_get_issue( + DOMAIN, f"{REPAIR_ISSUE_ENTITY_VALIDATION_ERROR}_{Platform.SWITCH}" + ) + assert issue is not None + assert issue.severity is ir.IssueSeverity.ERROR + assert issue.translation_placeholders == { + "platform": Platform.SWITCH, + "entities": f"- {INVALID_SWITCH_UID}", + } + + +async def test_load_applies_schema_defaults_and_coercion( + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, +) -> None: + """Test stored configs are normalized on load. + + The light in the fixture predates `color_temp_min` / `color_temp_max`, which + `KnxUiLight.__init__` reads by direct key access, and the switch stores + `entity_category` as a plain string. + """ + await knx.setup_integration( + config_store_fixture="config_store_invalid.json", state_updater=False + ) + assert hass.states.get("light.missing_defaults") is not None + config_store = hass.data[KNX_MODULE_KEY].config_store + light_config = config_store.get_entity_configs(Platform.LIGHT)[LIGHT_UID][DOMAIN] + assert light_config["color_temp_min"] == 2700 + assert light_config["color_temp_max"] == 6000 + + switch_id = entity_registry.async_get_entity_id( + Platform.SWITCH, DOMAIN, VALID_SWITCH_UID + ) + assert entity_registry.async_get(switch_id).entity_category is EntityCategory.CONFIG + + +async def test_load_valid_store_creates_no_issue( + hass: HomeAssistant, + knx: KNXTestKit, + issue_registry: ir.IssueRegistry, +) -> None: + """Test a valid store doesn't raise a repair issue.""" + await knx.setup_integration( + config_store_fixture="config_store_light_switch.json", state_updater=False + ) + assert not [ + issue + for issue in issue_registry.issues.values() + if issue.issue_id.startswith(REPAIR_ISSUE_ENTITY_VALIDATION_ERROR) + ] + + ########### # MIGRATION ########### diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index 1bdea26fabf0..eb631b8b1ec5 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -281,7 +281,7 @@ async def test_number_ui_load(knx: KNXTestKit) -> None: ) knx.assert_state( "number.test_options", - "3000", + "3000.0", # `min`, `max` and `step` are floats after validation unit_of_measurement="kW", device_class="power", min=3000,