构建 Home Assistant 自定义组件(第四部分):选项流程

构建 Home Assistant 自定义组件(第四部分):选项流程
引言
在本文中,我们将为自定义组件添加选项流程(Options Flow)。我们仍使用相同的示例项目 github - custom - component - tutorial。你可以在 feature/part4 分支上找到本文的差异。
选项流程允许用户随时通过导航到集成页面并点击组件卡片上的 “选项” 按钮来配置组件的其他选项。一般来说,这些配置值是可选的,而配置流程中的值是使组件正常工作所必需的。
我强烈建议在继续本教程之前阅读官方文档。
启用选项支持
根据文档,第一步是在配置流程类上定义一个方法,让它知道组件支持选项。在我们的例子中,我们将把这个方法添加到GithubCustomConfigFlow类中。

@staticmethod
@callback
def async_get_options_flow(config_entry):
    """Get the options flow for this handler."""
    return OptionsFlowHandler(config_entry)

与官方文档稍有不同的是,我们的OptionsFlowHandler类在初始化时需要配置项实例。这对于你可能编写的几乎每个组件都是必需的,因为我们将使用config_entry的options属性为选项流程表单填充默认值。
在 strings.json 中配置字段和错误
与配置流程一样,我们需要在strings.json中命名我们的数据字段和错误消息。这些将嵌套在options键下。你需要为在translations目录中选择支持的每种语言添加这些内容。

{
  "error": {
    "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name` and should be a valid github repository."
  },
  "step": {
    "init": {
      "title": "Manage Repos",
      "data": {
        "repos": "Existing Repos: Uncheck any repos you want to remove.",
        "path": "New Repo: Path to the repository e.g. home-assistant-core",
        "name": "New Repo: Name of the sensor."
      },
      "description": "Remove existing repos or add a new repo."
    }
  }
}

定义选项流程处理程序
下一步是编写处理选项流程的类。这与我们为配置流程定义的类非常相似,所以应该很熟悉。为了简洁起见,我将省略类中的大部分逻辑,尽量简化以展示重要部分。我将概述它的工作原理,然后深入探讨为我们的教程组件添加的特定逻辑。

class OptionsFlowHandler(config_entries.OptionsFlow):
    """Handles options flow for the component."""

    def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
        self.config_entry = config_entry

    async def async_step_init(
        self, user_input: Dict[str, Any] = None
    ) -> Dict[str, Any]:
        """Manage the options for the custom component."""
        errors: Dict[str, str] = {}
        # 从实体注册表中获取所有已配置的存储库,以便我们可以填充多选下拉列表,允许用户删除存储库
        entity_registry = await async_get_registry(self.hass)
        entries = async_entries_for_config_entry(
            entity_registry, self.config_entry.entry_id
        )
        # 多选的默认值
        all_repos = {e.entity_id: e.original_name for e in entries}
        repo_map = {e.entity_id: e for e in entries}

        if user_input is not None:
            # 为简洁起见,省略验证和其他处理逻辑
            #...
            if not errors:
                # 数据的值将设置在我们的config_entry实例的options属性上
                return self.async_create_entry(
                    title="",
                    data={CONF_REPOS: updated_repos},
                )

        options_schema = vol.Schema(
            {
                vol.Optional("repos", default=list(all_repos.keys())): cv.multi_select(
                    all_repos
                ),
                vol.Optional(CONF_PATH): cv.string,
                vol.Optional(CONF_NAME): cv.string,
            }
        )
        return self.async_show_form(
            step_id="init", data_schema=options_schema, errors=errors
        )

重写__init__
我们必须重写__init__,以便它可以接受一个config_entry实例,并将其设置为类的属性。如前所述,这样我们就可以访问它的options属性,以便在选项流程表单中预填充数据。

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
    self.config_entry = config_entry

定义选项数据模式
接下来我们定义选项数据模式。这与我们定义配置流程模式的方式相同。我们在方法本身中定义模式,以便为repos键提供默认值,该默认值在此方法中动态计算。如果你不需要任何动态值,可以像配置流程模式一样在上面定义为常量。

options_schema = vol.Schema(
    {
        vol.Optional("repos", default=list(all_repos.keys())): cv.multi_select(
            all_repos
        ),
        vol.Optional(CONF_PATH): cv.string,
        vol.Optional(CONF_NAME): cv.string,
    }
)

虽然我的组件中模式的其他键没有使用默认值,但通常你会从配置项实例中查找现有选项值,为表单设置默认值。例如:
python
复制
vol.Optional(CONF_PATH, default=self.config_entry.options[CONF_PATH])
显示选项表单
这里没有我们在配置流程中没有见过的新内容。与配置流程不同的一点是,选项流程只有一个名为init的步骤。

return self.async_show_form(
    step_id="init", data_schema=options_schema, errors=errors
)

保存选项数据
当用户提交的user_input验证通过后,我们可以通过返回async_create_entry方法来格式化和保存选项数据。

# 数据的值将设置在我们的config_entry实例的options属性上
return self.async_create_entry(
    title="",
    data={some_option: user_input["some_option"]},
)

注册选项更新监听器
为了让我们的组件知道选项已更改并能够对其采取行动,我们必须在初始设置配置项时注册并更新监听器。在我们的__init__.py文件中,我们将定义更新监听器函数并将其注册到配置项中。

async def options_update_listener(
    hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
    """Handle options update."""
    await hass.config_entries.async_reload(config_entry.entry_id)

如你所见,监听器的逻辑非常简单。它只是重新加载配置项,以便它可以对保存的新选项数据采取行动。然后我们必须在async_setup_entry函数中注册监听器。

hass_data = dict(entry.data)
# 注册更新监听器,以便在选项更新时更新配置项
unsub_options_update_listener = entry.add_update_listener(options_update_listener)
# 存储取消订阅函数的引用,以便在条目卸载时进行清理
hass_data["unsub_options_update_listener"] = unsub_options_update_listener
hass.data[DOMAIN][entry.entry_id] = hass_data

add_updated_listener方法返回一个取消订阅函数,我们将存储它,以便在用户删除配置项时清理监听器。
需要注意的是,只有当我们在 Options Flow 类中传递给self.async_create_entry的数据与之前不同时,更新监听器函数才会被调用。如果没有更改,选项更新监听器将不会被调用,你的配置项也不会被重新加载。
在设置期间使用选项值
现在我们已经设置了选项流程,用户可以输入值,这些值将保存在配置项实例上。最后一步是在设置平台时使用这些值。在我们的sensor.py中,我们可以使用选项值来更改传感器的设置方式。例如,可能如下所示:

async def async_setup_entry(
    hass: core.HomeAssistant,
    config_entry: config_entries.ConfigEntry,
    async_add_entities,
):
    """Setup sensors from a config entry created in the integrations UI."""
    config = hass.data[DOMAIN][config_entry.entry_id]
    some_option = config_entry.options.get("some_option")
    session = async_get_clientsession(hass)
    github = GitHubAPI(session, "requester", oauth_token=config[CONF_ACCESS_TOKEN])
    sensors = [GitHubRepoSensor(github, repo, some_option) for repo in config[CONF_REPOS]]
    async_add_entities(sensors, update_before_add=True)

Github 自定义组件中的选项流程
现在我已经介绍了使用选项流程的一般信息,我想回到我们在本教程中一直在构建的自定义组件。我添加的选项流程执行了我在其他选项流程中未见过的操作。主要是它允许删除已添加的存储库以及通过选项流程表单添加新存储库。下面你可以看到它的截图。
选项流程
多选允许用户取消选中他们想要删除的存储库。其他两个输入允许用户添加新存储库并为其提供可选名称。点击 “提交” 将删除未选中的存储库,并在指定新存储库时添加它。
删除存储库
在我们的选项流程类中,删除存储库的逻辑如下:

updated_repos = deepcopy(self.config_entry.data[CONF_REPOS])
# 删除任何未选中的存储库
removed_entities = [
    entity_id
    for entity_id in repo_map.keys()
    if entity_id not in user_input["repos"]
]
for entity_id in removed_entities:
    # 从Home Assistant中取消注册
    entity_registry.async_remove(entity_id)
    # 从我们已配置的存储库中删除
    entry = repo_map[entity_id]
    entry_path = entry.unique_id
    updated_repos = [e for e in updated_repos if e["path"]!= entry_path]

我们首先通过比较所选存储库与最初在config_entry中配置的存储库来确定哪些存储库未被选中。然后我们遍历每个entity_id,首先从实体注册表中删除它,然后从我们最初配置的存储库列表中删除。
添加存储库
如果用户为path输入提供了有效值,我们将添加一个新存储库。该逻辑如下所示:

if user_input.get(CONF_PATH):
    # 验证路径
    access_token = self.hass.data[DOMAIN][self.config_entry.entry_id][
        CONF_ACCESS_TOKEN
    ]
    try:
        await validate_path(user_input[CONF_PATH], access_token, self.hass)
    except ValueError:
        errors["base"] = "invalid_path"

    if not errors:
        # 添加新存储库
        updated_repos.append(
            {
                "path": user_input[CONF_PATH],
                "name": user_input.get(CONF_NAME, user_input[CONF_PATH]),
            }
        )

如果提供了值,我们首先验证它以确保它是一个真实的 GitHub 存储库。如果不是,我们用strings.json中定义的错误键填充errors字典。如果没有错误,我们只需将新存储库附加到现有列表中。
更新传感器
当我们从选项流程处理程序成功返回时,它将传递更新后的存储库列表作为data关键字参数。这个dict将设置在我们config_entry实例的options属性中。

return self.async_create_entry(
    title="",
    data={CONF_REPOS: updated_repos},
)

我们将在sensor.py中设置传感器时访问该数据。在创建传感器之前,我们用更新后的存储库扩充初始配置数据,这些存储库可能在配置流程中的初始配置之后被删除或添加。

diff --git a/custom_components/github_custom/sensor.py b/custom_components/github_custom/sensor.py
index 9a62f8a..c893fa2 100644
--- a/custom_components/github_custom/sensor.py
+++ b/custom_components/github_custom/sensor.py
@@ -70,6 +70,9 @@ async def async_setup_entry(
 ):
     """Setup sensors from a config entry created in the integrations UI."""
     config = hass.data[DOMAIN][config_entry.entry_id]
+    # 更新我们的配置以包括新存储库并删除已删除的存储库
+    if config_entry.options:
+        config.update(config_entry.options)
     session = async_get_clientsession(hass)
     github = GitHubAPI(session, "requester", oauth_token=config[CONF_ACCESS_TOKEN])
     sensors = [GitHubRepoSensor(github, repo) for repo in config[CONF_REPOS]]

单元测试
单元测试选项流程与测试配置流程没有太大不同,但需要一些额外的步骤。下面的测试测试了用户从配置项中取消选中现有存储库的情况。

@patch("custom_components.github_custom.sensor.GitHubAPI")
async def test_options_flow_remove_repo(m_github, hass):
    """Test config flow options."""
    m_instance = AsyncMock()
    m_instance.getitem = AsyncMock()
    m_github.return_value = m_instance

    config_entry = MockConfigEntry(
        domain=DOMAIN,
        unique_id="kodi_recently_added_media",
        data={
            CONF_ACCESS_TOKEN: "access-token",
            CONF_REPOS: [{"path": "home-assistant/core", "name": "HA Core"}],
        },
    )
    config_entry.add_to_hass(hass)
    assert await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    # 显示初始表单
    result = await hass.config_entries.options.async_init(config_entry.entry_id)
    # 提交带有选项的表单
    result = await hass.config_entries.options.async_configure(
        result["flow_id"], user_input={"repos": []}
    )
    assert "create_entry" == result["type"]
    assert "" == result["title"]
    assert result["result"] is True
    assert {CONF_REPOS: []} == result["data"]

我们首先需要创建一个模拟配置项并将其添加到 Home Assistant 中。接下来,我们生成初始选项流程并捕获流程 ID。在调用hass.config_entries.options.async_configure并传入我们的user_input数据时使用流程 ID。在这种情况下,我们模拟取消选中唯一配置的存储库。
下一步
此时,我们现在有一个功能完备的自定义组件,可以通过配置 UI 或configuration.yaml文件进行配置。在本系列的最后一篇文章中,我将简要介绍如何使用 Home Assistant 提供的 Visual Studio Code 开发容器在本地测试和调试你的组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值