From 759fe541329a8d675b7e662726b2559598570012 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 25 Oct 2024 16:25:41 +0200 Subject: [PATCH] Fix transition config storage in LCN light and scene platform (#127847) --- homeassistant/components/lcn/__init__.py | 25 +- homeassistant/components/lcn/config_flow.py | 4 +- homeassistant/components/lcn/light.py | 2 +- homeassistant/components/lcn/scene.py | 2 +- homeassistant/components/lcn/schemas.py | 11 +- .../lcn/fixtures/config_entry_pchk.json | 8 +- .../lcn/fixtures/config_entry_pchk_v1_1.json | 2 +- .../lcn/fixtures/config_entry_pchk_v1_2.json | 231 ++++++++++++++++++ tests/components/lcn/test_init.py | 20 +- tests/components/lcn/test_scene.py | 2 +- 10 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 tests/components/lcn/fixtures/config_entry_pchk_v1_2.json diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index a8d75fe5635..5995e06efcc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -11,10 +11,13 @@ from pypck.connection import PchkConnectionManager from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITIES, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -24,7 +27,9 @@ from .const import ( ADD_ENTITIES_CALLBACKS, CONF_ACKNOWLEDGE, CONF_DIM_MODE, + CONF_DOMAIN_DATA, CONF_SK_NUM_TRIES, + CONF_TRANSITION, CONNECTION, DOMAIN, PLATFORMS, @@ -147,15 +152,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry.minor_version, ) - if config_entry.version == 1: - new_data = {**config_entry.data} + new_data = {**config_entry.data} + if config_entry.version == 1: + # update to 1.2 (add acknowledge flag) if config_entry.minor_version < 2: new_data[CONF_ACKNOWLEDGE] = False - hass.config_entries.async_update_entry( - config_entry, data=new_data, minor_version=2, version=1 - ) + # update to 2.1 (fix transitions for lights and switches) + new_entities_data = [*new_data[CONF_ENTITIES]] + for entity in new_entities_data: + if entity[CONF_DOMAIN] in [Platform.LIGHT, Platform.SCENE]: + if entity[CONF_DOMAIN_DATA][CONF_TRANSITION] is None: + entity[CONF_DOMAIN_DATA][CONF_TRANSITION] = 0 + entity[CONF_DOMAIN_DATA][CONF_TRANSITION] /= 1000.0 + new_data[CONF_ENTITIES] = new_entities_data + + hass.config_entries.async_update_entry( + config_entry, data=new_data, minor_version=1, version=2 + ) _LOGGER.debug( "Migration to configuration version %s.%s successful", diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index ca72b1ca53f..e78378a61b1 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -110,8 +110,8 @@ async def validate_connection(data: ConfigType) -> str | None: class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a LCN config flow.""" - VERSION = 1 - MINOR_VERSION = 2 + VERSION = 2 + MINOR_VERSION = 1 async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: """Import existing configuration from LCN.""" diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 943e3c69acf..9ec660325c8 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -90,7 +90,7 @@ class LcnOutputLight(LcnEntity, LightEntity): self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] self._transition = pypck.lcn_defs.time_to_ramp_value( - config[CONF_DOMAIN_DATA][CONF_TRANSITION] + config[CONF_DOMAIN_DATA][CONF_TRANSITION] * 1000.0 ) self.dimmable = config[CONF_DOMAIN_DATA][CONF_DIMMABLE] diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index 241493ec108..0f40926cf17 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -87,7 +87,7 @@ class LcnScene(LcnEntity, Scene): self.transition = None else: self.transition = pypck.lcn_defs.time_to_ramp_value( - config[CONF_DOMAIN_DATA][CONF_TRANSITION] + config[CONF_DOMAIN_DATA][CONF_TRANSITION] * 1000.0 ) async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index 0539e83dea8..5f0353b413e 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -95,7 +95,7 @@ DOMAIN_DATA_LIGHT: VolDictType = { vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), vol.Optional(CONF_TRANSITION, default=0): vol.All( - vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000 + vol.Coerce(float), vol.Range(min=0.0, max=486.0) ), } @@ -106,13 +106,8 @@ DOMAIN_DATA_SCENE: VolDictType = { vol.Optional(CONF_OUTPUTS, default=[]): vol.All( cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))] ), - vol.Optional(CONF_TRANSITION, default=None): vol.Any( - vol.All( - vol.Coerce(int), - vol.Range(min=0.0, max=486.0), - lambda value: value * 1000, - ), - None, + vol.Optional(CONF_TRANSITION, default=0): vol.Any( + vol.All(vol.Coerce(int), vol.Range(min=0.0, max=486.0)) ), } diff --git a/tests/components/lcn/fixtures/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json index d8eef6d1eb3..778e6526a8f 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk.json +++ b/tests/components/lcn/fixtures/config_entry_pchk.json @@ -32,7 +32,7 @@ "domain_data": { "output": "OUTPUT1", "dimmable": true, - "transition": 5000.0 + "transition": 5.0 } }, { @@ -43,7 +43,7 @@ "domain_data": { "output": "OUTPUT2", "dimmable": false, - "transition": 0 + "transition": 0.0 } }, { @@ -145,7 +145,7 @@ "register": 0, "scene": 0, "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": null + "transition": 0.0 } }, { @@ -157,7 +157,7 @@ "register": 0, "scene": 1, "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": 10 + "transition": 10.0 } }, { diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json index 9a8095ff16d..b1ea494af42 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json @@ -156,7 +156,7 @@ "register": 0, "scene": 1, "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": 10 + "transition": 10000 } }, { diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json new file mode 100644 index 00000000000..902370c079f --- /dev/null +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json @@ -0,0 +1,231 @@ +{ + "host": "pchk", + "ip_address": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "STEPS200", + "acknowledge": false, + "devices": [ + { + "address": [0, 7, false], + "name": "TestModule", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + }, + { + "address": [0, 5, true], + "name": "TestGroup", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + } + ], + "entities": [ + { + "address": [0, 7, false], + "name": "Light_Output1", + "resource": "output1", + "domain": "light", + "domain_data": { + "output": "OUTPUT1", + "dimmable": true, + "transition": 5000.0 + } + }, + { + "address": [0, 7, false], + "name": "Light_Output2", + "resource": "output2", + "domain": "light", + "domain_data": { + "output": "OUTPUT2", + "dimmable": false, + "transition": 0 + } + }, + { + "address": [0, 7, false], + "name": "Light_Relay1", + "resource": "relay1", + "domain": "light", + "domain_data": { + "output": "RELAY1", + "dimmable": false, + "transition": 0.0 + } + }, + { + "address": [0, 7, false], + "name": "Switch_Output1", + "resource": "output1", + "domain": "switch", + "domain_data": { + "output": "OUTPUT1" + } + }, + { + "address": [0, 7, false], + "name": "Switch_Output2", + "resource": "output2", + "domain": "switch", + "domain_data": { + "output": "OUTPUT2" + } + }, + { + "address": [0, 7, false], + "name": "Switch_Relay1", + "resource": "relay1", + "domain": "switch", + "domain_data": { + "output": "RELAY1" + } + }, + { + "address": [0, 7, false], + "name": "Switch_Relay2", + "resource": "relay2", + "domain": "switch", + "domain_data": { + "output": "RELAY2" + } + }, + { + "address": [0, 5, true], + "name": "Switch_Group5", + "resource": "relay1", + "domain": "switch", + "domain_data": { + "output": "RELAY1" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Outputs", + "resource": "outputs", + "domain": "cover", + "domain_data": { + "motor": "OUTPUTS", + "reverse_time": "RT1200" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Relays", + "resource": "motor1", + "domain": "cover", + "domain_data": { + "motor": "MOTOR1", + "reverse_time": "RT1200" + } + }, + { + "address": [0, 7, false], + "name": "Climate1", + "resource": "var1.r1varsetpoint", + "domain": "climate", + "domain_data": { + "source": "VAR1", + "setpoint": "R1VARSETPOINT", + "lockable": true, + "min_temp": 0.0, + "max_temp": 40.0, + "unit_of_measurement": "°C" + } + }, + { + "address": [0, 7, false], + "name": "Romantic", + "resource": "0.0", + "domain": "scene", + "domain_data": { + "register": 0, + "scene": 0, + "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], + "transition": null + } + }, + { + "address": [0, 7, false], + "name": "Romantic Transition", + "resource": "0.1", + "domain": "scene", + "domain_data": { + "register": 0, + "scene": 1, + "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], + "transition": 10000 + } + }, + { + "address": [0, 7, false], + "name": "Sensor_LockRegulator1", + "resource": "r1varsetpoint", + "domain": "binary_sensor", + "domain_data": { + "source": "R1VARSETPOINT" + } + }, + { + "address": [0, 7, false], + "name": "Binary_Sensor1", + "resource": "binsensor1", + "domain": "binary_sensor", + "domain_data": { + "source": "BINSENSOR1" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_KeyLock", + "resource": "a5", + "domain": "binary_sensor", + "domain_data": { + "source": "A5" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Var1", + "resource": "var1", + "domain": "sensor", + "domain_data": { + "source": "VAR1", + "unit_of_measurement": "°C" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Setpoint1", + "resource": "r1varsetpoint", + "domain": "sensor", + "domain_data": { + "source": "R1VARSETPOINT", + "unit_of_measurement": "°C" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Led6", + "resource": "led6", + "domain": "sensor", + "domain_data": { + "source": "LED6", + "unit_of_measurement": "NATIVE" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_LogicOp1", + "resource": "logicop1", + "domain": "sensor", + "domain_data": { + "source": "LOGICOP1", + "unit_of_measurement": "NATIVE" + } + } + ] +} diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 62fa79961cb..1bd225c5d47 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -139,6 +139,22 @@ async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: entry_migrated = hass.config_entries.async_get_entry(entry_v1_1.entry_id) assert entry_migrated.state is ConfigEntryState.LOADED - assert entry_migrated.version == 1 - assert entry_migrated.minor_version == 2 + assert entry_migrated.version == 2 + assert entry_migrated.minor_version == 1 + assert entry_migrated.data == entry.data + + +@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +async def test_migrate_1_2(hass: HomeAssistant, entry) -> None: + """Test migration config entry.""" + entry_v1_2 = create_config_entry("pchk_v1_2", version=(1, 2)) + entry_v1_2.add_to_hass(hass) + + await hass.config_entries.async_setup(entry_v1_2.entry_id) + await hass.async_block_till_done() + + entry_migrated = hass.config_entries.async_get_entry(entry_v1_2.entry_id) + assert entry_migrated.state is ConfigEntryState.LOADED + assert entry_migrated.version == 2 + assert entry_migrated.minor_version == 1 assert entry_migrated.data == entry.data diff --git a/tests/components/lcn/test_scene.py b/tests/components/lcn/test_scene.py index fcd59693479..27e7864df41 100644 --- a/tests/components/lcn/test_scene.py +++ b/tests/components/lcn/test_scene.py @@ -51,7 +51,7 @@ async def test_scene_activate( assert state is not None activate_scene.assert_awaited_with( - 0, 0, [OutputPort.OUTPUT1, OutputPort.OUTPUT2], [RelayPort.RELAY1], None + 0, 0, [OutputPort.OUTPUT1, OutputPort.OUTPUT2], [RelayPort.RELAY1], 0.0 )