Sfoglia il codice sorgente

Add options-flow step to edit lock membership within an existing group

Previously groups could only be added or removed wholesale; editing
membership required removing and re-adding a group (risking an entity
name/id change for its sensors).
ZiB 3 mesi fa
parent
commit
933f9bcc32

+ 66 - 1
custom_components/zlock_slots/config_flow.py

@@ -68,11 +68,19 @@ class ZLockSlotsOptionsFlow(OptionsFlow):
     self._door_sensor_map: dict[str, str] = dict(
       config_entry.options.get(CONF_DOOR_SENSOR_MAP, {})
     )
+    self._edit_group_name: str | None = None
 
   async def async_step_init(self, user_input=None) -> ConfigFlowResult:
     return self.async_show_menu(
       step_id="init",
-      menu_options=["manage_locks", "associate_sensors", "add_group", "remove_group", "finish"],
+      menu_options=[
+        "manage_locks",
+        "associate_sensors",
+        "add_group",
+        "edit_group",
+        "remove_group",
+        "finish",
+      ],
     )
 
   async def async_step_manage_locks(self, user_input=None) -> ConfigFlowResult:
@@ -159,6 +167,63 @@ class ZLockSlotsOptionsFlow(OptionsFlow):
     })
     return self.async_show_form(step_id="add_group", data_schema=schema, errors=errors)
 
+  async def async_step_edit_group(self, user_input=None) -> ConfigFlowResult:
+    if not self._groups:
+      return await self.async_step_init()
+
+    if len(self._groups) == 1:
+      self._edit_group_name = self._groups[0][CONF_GROUP_NAME]
+      return await self.async_step_edit_group_locks()
+
+    if user_input is not None:
+      self._edit_group_name = user_input["group"]
+      return await self.async_step_edit_group_locks()
+
+    schema = vol.Schema({
+      vol.Required("group"): selector.SelectSelector(
+        selector.SelectSelectorConfig(
+          options=[
+            selector.SelectOptionDict(value=g[CONF_GROUP_NAME], label=g[CONF_GROUP_NAME])
+            for g in self._groups
+          ],
+        )
+      ),
+    })
+    return self.async_show_form(step_id="edit_group", data_schema=schema)
+
+  async def async_step_edit_group_locks(self, user_input=None) -> ConfigFlowResult:
+    errors: dict[str, str] = {}
+    group = next(g for g in self._groups if g[CONF_GROUP_NAME] == self._edit_group_name)
+
+    if user_input is not None:
+      selected: list[str] = user_input.get(CONF_LOCK_ENTITY_IDS, [])
+      if not selected:
+        errors[CONF_LOCK_ENTITY_IDS] = "no_locks_selected"
+      else:
+        group[CONF_LOCK_ENTITY_IDS] = selected
+        return await self.async_step_init()
+
+    lock_options = [
+      selector.SelectOptionDict(
+        value=lid,
+        label=(self.hass.states.get(lid).name if self.hass.states.get(lid) else lid),
+      )
+      for lid in self._lock_ids
+    ]
+    schema = vol.Schema({
+      vol.Required(
+        CONF_LOCK_ENTITY_IDS, default=group[CONF_LOCK_ENTITY_IDS]
+      ): selector.SelectSelector(
+        selector.SelectSelectorConfig(options=lock_options, multiple=True)
+      ),
+    })
+    return self.async_show_form(
+      step_id="edit_group_locks",
+      description_placeholders={"group_name": self._edit_group_name},
+      data_schema=schema,
+      errors=errors,
+    )
+
   async def async_step_remove_group(self, user_input=None) -> ConfigFlowResult:
     if not self._groups:
       return await self.async_step_init()

+ 14 - 0
custom_components/zlock_slots/strings.json

@@ -21,6 +21,7 @@
           "manage_locks": "Manage locks",
           "associate_sensors": "Associate door sensors",
           "add_group": "Add group",
+          "edit_group": "Edit group",
           "remove_group": "Remove group",
           "finish": "Save and close"
         }
@@ -40,6 +41,19 @@
           "lock_entity_ids": "Locks in group"
         }
       },
+      "edit_group": {
+        "title": "Edit Lock Group",
+        "data": {
+          "group": "Group to edit"
+        }
+      },
+      "edit_group_locks": {
+        "title": "Edit Lock Group",
+        "description": "Add or remove locks in the \"{group_name}\" group.",
+        "data": {
+          "lock_entity_ids": "Locks in group"
+        }
+      },
       "associate_sensors": {
         "title": "Associate Door Sensors",
         "description": "Optionally link a binary sensor (door contact) to each lock. This enables door state and combined lock+door state sensors."

+ 14 - 0
custom_components/zlock_slots/translations/en.json

@@ -21,6 +21,7 @@
           "manage_locks": "Manage locks",
           "associate_sensors": "Associate door sensors",
           "add_group": "Add group",
+          "edit_group": "Edit group",
           "remove_group": "Remove group",
           "finish": "Save and close"
         }
@@ -40,6 +41,19 @@
           "lock_entity_ids": "Locks in group"
         }
       },
+      "edit_group": {
+        "title": "Edit Lock Group",
+        "data": {
+          "group": "Group to edit"
+        }
+      },
+      "edit_group_locks": {
+        "title": "Edit Lock Group",
+        "description": "Add or remove locks in the \"{group_name}\" group.",
+        "data": {
+          "lock_entity_ids": "Locks in group"
+        }
+      },
       "associate_sensors": {
         "title": "Associate Door Sensors",
         "description": "Optionally link a binary sensor (door contact) to each lock. This enables door state and combined lock+door state sensors."

+ 62 - 0
tests/test_config_flow.py

@@ -174,6 +174,68 @@ async def test_options_group_dropped_when_all_its_locks_removed(hass):
   assert entry.options[CONF_GROUPS] == []
 
 
+async def test_options_edit_group_single_group_skips_selection(hass):
+  entry = _entry_with_group()
+  await _setup_entry(hass, entry)
+
+  result = await _begin_options(hass, entry, "edit_group")
+  assert result["type"] is FlowResultType.FORM
+  assert result["step_id"] == "edit_group_locks"
+
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
+  )
+  assert result["type"] is FlowResultType.MENU
+  result = await _finish(hass, result["flow_id"])
+
+  assert result["type"] is FlowResultType.CREATE_ENTRY
+  assert entry.options[CONF_GROUPS] == [
+    {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
+  ]
+
+
+async def test_options_edit_group_multiple_groups_prompts_for_selection(hass):
+  entry = MockConfigEntry(
+    domain=DOMAIN,
+    data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
+    options={
+      CONF_GROUPS: [
+        {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
+        {CONF_GROUP_NAME: "Front only", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
+      ]
+    },
+    entry_id="test_entry_edit_group",
+  )
+  await _setup_entry(hass, entry)
+
+  result = await _begin_options(hass, entry, "edit_group")
+  assert result["type"] is FlowResultType.FORM
+  assert result["step_id"] == "edit_group"
+
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"], {"group": "Front only"}
+  )
+  assert result["type"] is FlowResultType.FORM
+  assert result["step_id"] == "edit_group_locks"
+
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}
+  )
+  result = await _finish(hass, result["flow_id"])
+
+  assert entry.options[CONF_GROUPS] == [
+    {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
+    {CONF_GROUP_NAME: "Front only", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
+  ]
+
+
+async def test_options_edit_group_no_groups_returns_to_menu(hass, config_entry):
+  await _setup_entry(hass, config_entry)
+
+  result = await _begin_options(hass, config_entry, "edit_group")
+  assert result["type"] is FlowResultType.MENU
+
+
 async def test_options_abandoned_flow_changes_nothing(hass):
   """Lock changes are staged on the flow; without 'finish' the entry is untouched."""
   entry = _entry_with_group()