diff --git a/custom_components/heating_mpc/config_flow.py b/custom_components/heating_mpc/config_flow.py index f60ebce..789a9a7 100644 --- a/custom_components/heating_mpc/config_flow.py +++ b/custom_components/heating_mpc/config_flow.py @@ -63,11 +63,21 @@ def _weather_schema(config: dict[str, Any], metrics: tuple[str, ...]): # A temperature source is mandatory: other weather inputs are optional. marker = vol.Required(key, default=selected) if selected else vol.Required(key) else: - marker = vol.Optional(key, default=selected) + # A default would re-insert old sources whenever a cleared field is + # omitted from the submission; a suggested value only pre-fills the form. + marker = vol.Optional(key, description={"suggested_value": selected}) schema[marker] = _entity_picker(multiple=True) return vol.Schema(schema) +def _with_cleared_weather(user_input: dict[str, Any], metrics: tuple[str, ...]) -> dict[str, Any]: + """Store an omitted optional source list as empty, so it overrides older data.""" + result = dict(user_input) + for metric in metrics: + result.setdefault(WEATHER_SOURCES[metric], []) + return result + + def _weight_schema(rooms: list[str], existing: dict[str, float] | None = None): weights = existing or {} return vol.Schema({ @@ -110,14 +120,14 @@ class HeatingMPCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not _valid_weather(self._draft[CONF_ROOMS], user_input): return self.async_show_form(step_id="weather", data_schema=_weather_schema( user_input, WEATHER_MAIN), errors={"base": "invalid_weather"}) - self._draft.update(user_input) + self._draft.update(_with_cleared_weather(user_input, WEATHER_MAIN)) return await self.async_step_weather_extra() return self.async_show_form(step_id="weather", data_schema=_weather_schema( self._draft, WEATHER_MAIN)) async def async_step_weather_extra(self, user_input: dict[str, Any] | None = None): if user_input is not None: - self._draft.update(user_input) + self._draft.update(_with_cleared_weather(user_input, WEATHER_EXTRA)) return await self.async_step_weights() return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema( self._draft, WEATHER_EXTRA)) @@ -161,7 +171,7 @@ class HeatingMPCOptionsFlow(config_entries.OptionsFlow): if not _valid_weather(self._draft[CONF_ROOMS], user_input): return self.async_show_form(step_id="weather", data_schema=_weather_schema( user_input, WEATHER_MAIN), errors={"base": "invalid_weather"}) - self._draft.update(user_input) + self._draft.update(_with_cleared_weather(user_input, WEATHER_MAIN)) return await self.async_step_weather_extra() return self.async_show_form(step_id="weather", data_schema=_weather_schema( config, WEATHER_MAIN)) @@ -169,7 +179,7 @@ class HeatingMPCOptionsFlow(config_entries.OptionsFlow): async def async_step_weather_extra(self, user_input: dict[str, Any] | None = None): config = {**self.config_entry.data, **self.config_entry.options, **self._draft} if user_input is not None: - self._draft.update(user_input) + self._draft.update(_with_cleared_weather(user_input, WEATHER_EXTRA)) return await self.async_step_weights() return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema( config, WEATHER_EXTRA)) diff --git a/tests/ha/test_config_flow.py b/tests/ha/test_config_flow.py index f2afe46..8c8d99e 100644 --- a/tests/ha/test_config_flow.py +++ b/tests/ha/test_config_flow.py @@ -4,6 +4,7 @@ from homeassistant.data_entry_flow import FlowResultType from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.heating_mpc.const import DOMAIN +from custom_components.heating_mpc.weather_sources import sources_for async def test_config_flow(hass, inputs): @@ -56,3 +57,35 @@ async def test_v1_entry_migrates_on_setup(hass, inputs): assert entry.version == 2 assert entry.data["outside_temperature_sources"] == ["sensor.outside"] assert "outside_entity" not in entry.data + + +async def test_options_flow_clears_optional_weather_sources(hass, inputs): + """Swapping providers in the UI must allow removing an optional source list.""" + entry = MockConfigEntry(domain=DOMAIN, version=2, unique_id="home_heating", data={ + "climate_entity": "climate.reference", "heater_entity": "switch.boiler", + "room_entities": ["sensor.room"], "outside_temperature_sources": ["sensor.outside"], + "outside_wind_sources": ["sensor.wind"], "outside_pressure_sources": ["sensor.pressure"], + "weights": {"sensor.room": 1.0}}) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + flow = hass.config_entries.options + result = await flow.async_init(entry.entry_id) + result = await flow.async_configure(result["flow_id"], result["data_schema"]({ + "climate_entity": "climate.reference", "heater_entity": "switch.boiler", + "room_entities": ["sensor.room"]})) + markers = {str(key): key for key in result["data_schema"].schema} + assert markers["outside_wind_sources"].description == {"suggested_value": ["sensor.wind"]} + # The user removes the wind provider; a cleared field may be omitted on submit. + result = await flow.async_configure(result["flow_id"], result["data_schema"]( + {"outside_temperature_sources": ["sensor.outside"]})) + result = await flow.async_configure(result["flow_id"], result["data_schema"]( + {"outside_pressure_sources": ["sensor.pressure"]})) + result = await flow.async_configure(result["flow_id"], {"weight__sensor.room": 1}) + assert result["type"] is FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + assert entry.options["outside_wind_sources"] == [] + assert entry.options["outside_pressure_sources"] == ["sensor.pressure"] + config = hass.data[DOMAIN][entry.entry_id].config + assert sources_for(config, "wind") == [] + assert sources_for(config, "pressure") == ["sensor.pressure"]