Allow clearing optional weather sources in Options

Optional weather source fields used the current selection as the form
default. A cleared field that the frontend omits was refilled with the
old sources by voluptuous, and an options key missing from the
submission fell back to the original setup data. Optional providers
could therefore not be removed in the UI.

Pre-fill with suggested values instead, and save omitted optional lists
as empty so they override older entry data. No UI strings changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-26 21:21:38 +02:00
1 parent 2a0a94c640
commit 30f78fb6a9
2 files changed
+48 -5

No files matched your search

+15 -5
View File
@@ -63,11 +63,21 @@ def _weather_schema(config: dict[str, Any], metrics: tuple[str, ...]):
# A temperature source is mandatory: other weather inputs are optional. # A temperature source is mandatory: other weather inputs are optional.
marker = vol.Required(key, default=selected) if selected else vol.Required(key) marker = vol.Required(key, default=selected) if selected else vol.Required(key)
else: 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) schema[marker] = _entity_picker(multiple=True)
return vol.Schema(schema) 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): def _weight_schema(rooms: list[str], existing: dict[str, float] | None = None):
weights = existing or {} weights = existing or {}
return vol.Schema({ return vol.Schema({
@@ -110,14 +120,14 @@ class HeatingMPCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not _valid_weather(self._draft[CONF_ROOMS], user_input): if not _valid_weather(self._draft[CONF_ROOMS], user_input):
return self.async_show_form(step_id="weather", data_schema=_weather_schema( return self.async_show_form(step_id="weather", data_schema=_weather_schema(
user_input, WEATHER_MAIN), errors={"base": "invalid_weather"}) 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 await self.async_step_weather_extra()
return self.async_show_form(step_id="weather", data_schema=_weather_schema( return self.async_show_form(step_id="weather", data_schema=_weather_schema(
self._draft, WEATHER_MAIN)) self._draft, WEATHER_MAIN))
async def async_step_weather_extra(self, user_input: dict[str, Any] | None = None): async def async_step_weather_extra(self, user_input: dict[str, Any] | None = None):
if user_input is not 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 await self.async_step_weights()
return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema( return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema(
self._draft, WEATHER_EXTRA)) self._draft, WEATHER_EXTRA))
@@ -161,7 +171,7 @@ class HeatingMPCOptionsFlow(config_entries.OptionsFlow):
if not _valid_weather(self._draft[CONF_ROOMS], user_input): if not _valid_weather(self._draft[CONF_ROOMS], user_input):
return self.async_show_form(step_id="weather", data_schema=_weather_schema( return self.async_show_form(step_id="weather", data_schema=_weather_schema(
user_input, WEATHER_MAIN), errors={"base": "invalid_weather"}) 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 await self.async_step_weather_extra()
return self.async_show_form(step_id="weather", data_schema=_weather_schema( return self.async_show_form(step_id="weather", data_schema=_weather_schema(
config, WEATHER_MAIN)) 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): 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} config = {**self.config_entry.data, **self.config_entry.options, **self._draft}
if user_input is not 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 await self.async_step_weights()
return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema( return self.async_show_form(step_id="weather_extra", data_schema=_weather_schema(
config, WEATHER_EXTRA)) config, WEATHER_EXTRA))
+33
View File
@@ -4,6 +4,7 @@ from homeassistant.data_entry_flow import FlowResultType
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.heating_mpc.const import DOMAIN 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): 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.version == 2
assert entry.data["outside_temperature_sources"] == ["sensor.outside"] assert entry.data["outside_temperature_sources"] == ["sensor.outside"]
assert "outside_entity" not in entry.data 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"]