فهرست منبع

Options flow: replace linear wizard with section menu

Opens with a three-option menu (Sync, Defaults, Locks) so returning
users can jump directly to what they need. Each section saves
independently, preserving the other sections' stored options. The sync
branch still chains sync_mode → caldav/api since those are tightly
coupled.
ZiB 4 ماه پیش
والد
کامیت
48499ea0f4

+ 33 - 8
custom_components/room_cal_ha/config_flow.py

@@ -160,19 +160,44 @@ class RoomCalOptionsFlow(OptionsFlow):
   async def async_step_init(
     self, user_input: dict[str, Any] | None = None
   ) -> ConfigFlowResult:
-    return await self.async_step_sync_mode()
+    if user_input is not None:
+      self._pending_options = {**self.config_entry.options}
+      section = user_input["section"]
+      if section == "sync":
+        return await self.async_step_sync_mode()
+      if section == "defaults":
+        return await self.async_step_defaults()
+      return await self.async_step_locks()
+
+    opts = self.config_entry.options
+    current_mode = opts.get(CONF_SYNC_MODE, SYNC_MODE_MANUAL)
+    return self.async_show_form(
+      step_id="init",
+      data_schema=vol.Schema({
+        vol.Required("section"): SelectSelector(
+          SelectSelectorConfig(
+            options=[
+              {"value": "sync", "label": f"Sync Mode & Connection (currently: {current_mode})"},
+              {"value": "defaults", "label": "Reservation Defaults"},
+              {"value": "locks", "label": "Z-Wave Locks"},
+            ],
+            mode=SelectSelectorMode.LIST,
+          )
+        ),
+      }),
+    )
 
   async def async_step_sync_mode(
     self, user_input: dict[str, Any] | None = None
   ) -> ConfigFlowResult:
     if user_input is not None:
       self._selected_mode = user_input[CONF_SYNC_MODE]
+      self._pending_options[CONF_SYNC_MODE] = self._selected_mode
       if self._selected_mode == SYNC_MODE_CALDAV:
         return await self.async_step_caldav()
       if self._selected_mode == SYNC_MODE_API:
         return await self.async_step_api()
-      self._pending_options = {**self.config_entry.options, CONF_SYNC_MODE: self._selected_mode}
-      return await self.async_step_defaults()
+      return self.async_create_entry(data=self._pending_options)
 
     opts = self.config_entry.options
     return self.async_show_form(
@@ -216,8 +241,8 @@ class RoomCalOptionsFlow(OptionsFlow):
       if not errors:
         new_data = {**self.config_entry.data, CONF_USERNAME: username, CONF_PASSWORD: password}
         self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)
-        self._pending_options = {**self.config_entry.options, CONF_SYNC_MODE: self._selected_mode, **user_input}
-        return await self.async_step_defaults()
+        self._pending_options = {**self._pending_options, CONF_SYNC_MODE: self._selected_mode, **user_input}
+        return self.async_create_entry(data=self._pending_options)
 
     opts = self.config_entry.options
     return self.async_show_form(
@@ -262,11 +287,11 @@ class RoomCalOptionsFlow(OptionsFlow):
         new_data = {**self.config_entry.data, CONF_API_TOKEN: api_token}
         self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)
         self._pending_options = {
-          **self.config_entry.options,
+          **self._pending_options,
           CONF_SYNC_MODE: self._selected_mode,
           **user_input,
         }
-        return await self.async_step_defaults()
+        return self.async_create_entry(data=self._pending_options)
 
     opts = self.config_entry.options
     return self.async_show_form(
@@ -311,7 +336,7 @@ class RoomCalOptionsFlow(OptionsFlow):
           defaults[key] = None
       if not errors:
         self._pending_options[CONF_RESERVATION_DEFAULTS] = defaults
-        return await self.async_step_locks()
+        return self.async_create_entry(data=self._pending_options)
 
     existing = self.config_entry.options.get(CONF_RESERVATION_DEFAULTS, {})
     return self.async_show_form(

+ 7 - 0
custom_components/room_cal_ha/strings.json

@@ -34,6 +34,13 @@
   },
   "options": {
     "step": {
+      "init": {
+        "title": "Configure Options",
+        "description": "Choose which settings to update.",
+        "data": {
+          "section": "Section"
+        }
+      },
       "sync_mode": {
         "title": "Sync Mode",
         "data": {

+ 7 - 0
custom_components/room_cal_ha/translations/en.json

@@ -34,6 +34,13 @@
   },
   "options": {
     "step": {
+      "init": {
+        "title": "Configure Options",
+        "description": "Choose which settings to update.",
+        "data": {
+          "section": "Section"
+        }
+      },
       "sync_mode": {
         "title": "Sync Mode",
         "data": {

+ 107 - 94
tests/test_config_flow.py

@@ -37,8 +37,22 @@ _EMPTY_DEFAULTS = {
 _EMPTY_LOCKS = {CONF_ZWAVE_LOCKS: [], CONF_ZWAVE_SLOT: 1}
 
 
-async def _submit_locks_step(hass: HomeAssistant, flow_id: str, user_input: dict | None = None):
-  """Submit the locks step (last step in options flow) and return the result."""
+async def _options_init(hass: HomeAssistant, mock_entry):
+  """Start an options flow; returns the init-step result."""
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  assert result["step_id"] == "init"
+  return result
+
+
+async def _select_section(hass: HomeAssistant, flow_id: str, section: str):
+  """Choose a section from the init menu."""
+  return await hass.config_entries.options.async_configure(
+    flow_id, user_input={"section": section}
+  )
+
+
+async def _submit_locks(hass: HomeAssistant, flow_id: str, user_input: dict | None = None):
   return await hass.config_entries.options.async_configure(
     flow_id, user_input=user_input if user_input is not None else _EMPTY_LOCKS
   )
@@ -70,37 +84,56 @@ async def test_user_step_creates_entry_with_room_name(hass: HomeAssistant):
 
 
 # ---------------------------------------------------------------------------
-# Options flow — sync_mode step
+# Options flow — init (menu) step
 # ---------------------------------------------------------------------------
 
-async def test_options_sync_mode_manual(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+async def test_options_init_shows_form(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  assert result["type"] == "form"
+  assert result["step_id"] == "init"
+
+
+async def test_options_init_sync_leads_to_sync_mode(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
   assert result["type"] == "form"
   assert result["step_id"] == "sync_mode"
 
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"],
-    user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
-  )
+
+async def test_options_init_defaults_leads_to_defaults(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "defaults")
   assert result["type"] == "form"
   assert result["step_id"] == "defaults"
 
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"],
-    user_input=_EMPTY_DEFAULTS,
-  )
+
+async def test_options_init_locks_leads_to_locks(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "locks")
   assert result["type"] == "form"
   assert result["step_id"] == "locks"
 
-  result = await _submit_locks_step(hass, result["flow_id"])
+
+# ---------------------------------------------------------------------------
+# Options flow — sync section
+# ---------------------------------------------------------------------------
+
+async def test_options_sync_mode_manual_saves(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
+  assert result["step_id"] == "sync_mode"
+
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"],
+    user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
+  )
   assert result["type"] == "create_entry"
   assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_MANUAL
 
 
 async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
   result = await hass.config_entries.options.async_configure(
     result["flow_id"],
     user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV},
@@ -110,8 +143,8 @@ async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssist
 
 
 async def test_options_sync_mode_api_proceeds_to_api_step(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
   result = await hass.config_entries.options.async_configure(
     result["flow_id"],
     user_input={CONF_SYNC_MODE: SYNC_MODE_API},
@@ -124,12 +157,18 @@ async def test_options_sync_mode_api_proceeds_to_api_step(hass: HomeAssistant, m
 # Options flow — CalDAV step
 # ---------------------------------------------------------------------------
 
-async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
+async def _reach_caldav_step(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
+  result = await hass.config_entries.options.async_configure(
     result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
   )
+  assert result["step_id"] == "caldav"
+  return result
+
+
+async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
+  result = await _reach_caldav_step(hass, mock_entry)
 
   with patch("custom_components.room_cal_ha.config_flow._test_caldav_auth", return_value=None):
     result = await hass.config_entries.options.async_configure(
@@ -143,17 +182,6 @@ async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
       },
     )
 
-  assert result["type"] == "form"
-  assert result["step_id"] == "defaults"
-
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"],
-    user_input=_EMPTY_DEFAULTS,
-  )
-  assert result["type"] == "form"
-  assert result["step_id"] == "locks"
-
-  result = await _submit_locks_step(hass, result["flow_id"])
   assert result["type"] == "create_entry"
   assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
   assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"
@@ -161,11 +189,7 @@ async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
 
 
 async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
-  )
+  result = await _reach_caldav_step(hass, mock_entry)
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_caldav_auth",
@@ -187,11 +211,7 @@ async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, m
 
 
 async def test_options_caldav_connect_error_shows_error(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
-  )
+  result = await _reach_caldav_step(hass, mock_entry)
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_caldav_auth",
@@ -216,12 +236,18 @@ async def test_options_caldav_connect_error_shows_error(hass: HomeAssistant, moc
 # Options flow — API step
 # ---------------------------------------------------------------------------
 
-async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
+async def _reach_api_step(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "sync")
+  result = await hass.config_entries.options.async_configure(
     result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
   )
+  assert result["step_id"] == "api"
+  return result
+
+
+async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
+  result = await _reach_api_step(hass, mock_entry)
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_api_connection",
@@ -236,17 +262,6 @@ async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
       },
     )
 
-  assert result["type"] == "form"
-  assert result["step_id"] == "defaults"
-
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"],
-    user_input=_EMPTY_DEFAULTS,
-  )
-  assert result["type"] == "form"
-  assert result["step_id"] == "locks"
-
-  result = await _submit_locks_step(hass, result["flow_id"])
   assert result["type"] == "create_entry"
   assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_API
   assert result["data"][CONF_API_BASE_URL] == "https://api.example.com"
@@ -256,11 +271,7 @@ async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
 async def test_options_api_bad_token_shows_error(hass: HomeAssistant, mock_entry):
   from custom_components.room_cal_ha.config_flow import _ApiAuthError
 
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
-  )
+  result = await _reach_api_step(hass, mock_entry)
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_api_connection",
@@ -280,11 +291,7 @@ async def test_options_api_bad_token_shows_error(hass: HomeAssistant, mock_entry
 
 
 async def test_options_api_connect_error_shows_error(hass: HomeAssistant, mock_entry):
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
-  )
+  result = await _reach_api_step(hass, mock_entry)
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_api_connection",
@@ -308,26 +315,12 @@ async def test_options_api_connect_error_shows_error(hass: HomeAssistant, mock_e
 # ---------------------------------------------------------------------------
 
 async def _reach_defaults_step(hass: HomeAssistant, mock_entry):
-  """Navigate to the defaults step via manual sync mode."""
-  mock_entry.add_to_hass(hass)
-  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL}
-  )
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "defaults")
   assert result["step_id"] == "defaults"
   return result
 
 
-async def _reach_locks_step(hass: HomeAssistant, mock_entry):
-  """Navigate to the locks step via manual sync mode + empty defaults."""
-  result = await _reach_defaults_step(hass, mock_entry)
-  result = await hass.config_entries.options.async_configure(
-    result["flow_id"], user_input=_EMPTY_DEFAULTS
-  )
-  assert result["step_id"] == "locks"
-  return result
-
-
 async def test_options_defaults_saves_values(hass: HomeAssistant, mock_entry):
   result = await _reach_defaults_step(hass, mock_entry)
 
@@ -342,10 +335,6 @@ async def test_options_defaults_saves_values(hass: HomeAssistant, mock_entry):
       "door_code": "1234",
     },
   )
-  assert result["type"] == "form"
-  assert result["step_id"] == "locks"
-
-  result = await _submit_locks_step(hass, result["flow_id"])
   assert result["type"] == "create_entry"
   defaults = result["data"][CONF_RESERVATION_DEFAULTS]
   assert defaults["guest_name"] == "Default Guest"
@@ -363,10 +352,6 @@ async def test_options_defaults_empty_strings_become_none(hass: HomeAssistant, m
     result["flow_id"],
     user_input=_EMPTY_DEFAULTS,
   )
-  assert result["type"] == "form"
-  assert result["step_id"] == "locks"
-
-  result = await _submit_locks_step(hass, result["flow_id"])
   assert result["type"] == "create_entry"
   defaults = result["data"][CONF_RESERVATION_DEFAULTS]
   assert defaults["guest_name"] is None
@@ -389,6 +374,13 @@ async def test_options_defaults_invalid_time_shows_error(hass: HomeAssistant, mo
 # Options flow — locks step
 # ---------------------------------------------------------------------------
 
+async def _reach_locks_step(hass: HomeAssistant, mock_entry):
+  result = await _options_init(hass, mock_entry)
+  result = await _select_section(hass, result["flow_id"], "locks")
+  assert result["step_id"] == "locks"
+  return result
+
+
 async def test_options_locks_step_shows_form(hass: HomeAssistant, mock_entry):
   result = await _reach_locks_step(hass, mock_entry)
   assert result["type"] == "form"
@@ -410,6 +402,27 @@ async def test_options_locks_saves_entities_and_slot(hass: HomeAssistant, mock_e
 async def test_options_locks_empty_saves_empty_list(hass: HomeAssistant, mock_entry):
   result = await _reach_locks_step(hass, mock_entry)
 
-  result = await _submit_locks_step(hass, result["flow_id"])
+  result = await _submit_locks(hass, result["flow_id"])
   assert result["type"] == "create_entry"
   assert result["data"][CONF_ZWAVE_LOCKS] == []
+
+
+# ---------------------------------------------------------------------------
+# Cross-section preservation
+# ---------------------------------------------------------------------------
+
+async def test_options_editing_defaults_preserves_sync_mode(hass: HomeAssistant, mock_entry):
+  """Editing defaults alone should not wipe a previously configured sync mode."""
+  from homeassistant.config_entries import ConfigEntry
+  existing_options = {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com"}
+  mock_entry.add_to_hass(hass)
+  hass.config_entries.async_update_entry(mock_entry, options=existing_options)
+
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  result = await _select_section(hass, result["flow_id"], "defaults")
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input=_EMPTY_DEFAULTS
+  )
+  assert result["type"] == "create_entry"
+  assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
+  assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"