Add scaffold for helper integration config flow (#67803)
This commit is contained in:
parent
aa82f96c16
commit
f0dba8ec70
10 changed files with 316 additions and 0 deletions
|
@ -59,6 +59,8 @@ def main():
|
||||||
# If it's a new integration and it's not a config flow,
|
# If it's a new integration and it's not a config flow,
|
||||||
# create a config flow too.
|
# create a config flow too.
|
||||||
if not args.template.startswith("config_flow"):
|
if not args.template.startswith("config_flow"):
|
||||||
|
if info.helper:
|
||||||
|
template = "config_flow_helper"
|
||||||
if info.oauth2:
|
if info.oauth2:
|
||||||
template = "config_flow_oauth2"
|
template = "config_flow_oauth2"
|
||||||
elif info.authentication or not info.discoverable:
|
elif info.authentication or not info.discoverable:
|
||||||
|
|
|
@ -6,6 +6,10 @@ DATA = {
|
||||||
"title": "Config Flow",
|
"title": "Config Flow",
|
||||||
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html",
|
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html",
|
||||||
},
|
},
|
||||||
|
"config_flow_helper": {
|
||||||
|
"title": "Helper Config Flow",
|
||||||
|
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#helper",
|
||||||
|
},
|
||||||
"config_flow_discovery": {
|
"config_flow_discovery": {
|
||||||
"title": "Discoverable Config Flow",
|
"title": "Discoverable Config Flow",
|
||||||
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication",
|
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication",
|
||||||
|
|
|
@ -119,6 +119,11 @@ More info @ https://developers.home-assistant.io/docs/creating_integration_manif
|
||||||
"default": "no",
|
"default": "no",
|
||||||
**YES_NO,
|
**YES_NO,
|
||||||
},
|
},
|
||||||
|
"helper": {
|
||||||
|
"prompt": "Is this a helper integration? (yes/no)",
|
||||||
|
"default": "no",
|
||||||
|
**YES_NO,
|
||||||
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"prompt": "Can the user authenticate the device using OAuth2? (yes/no)",
|
"prompt": "Can the user authenticate the device using OAuth2? (yes/no)",
|
||||||
"default": "no",
|
"default": "no",
|
||||||
|
|
|
@ -150,6 +150,19 @@ def _custom_tasks(template, info: Info) -> None:
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif template == "config_flow_helper":
|
||||||
|
info.update_manifest(config_flow=True)
|
||||||
|
info.update_strings(
|
||||||
|
config={
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"description": "Select the sensor for the NEW_NAME.",
|
||||||
|
"data": {"entity_id": "Sensor entity"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
elif template == "config_flow_oauth2":
|
elif template == "config_flow_oauth2":
|
||||||
info.update_manifest(config_flow=True, dependencies=["http"])
|
info.update_manifest(config_flow=True, dependencies=["http"])
|
||||||
info.update_strings(
|
info.update_strings(
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""The NEW_NAME integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up NEW_NAME from a config entry."""
|
||||||
|
# TODO Optionally store an object for your platforms to access
|
||||||
|
# hass.data[DOMAIN][entry.entry_id] = ...
|
||||||
|
|
||||||
|
# TODO Optionally validate config entry options before setting up platform
|
||||||
|
|
||||||
|
hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,))
|
||||||
|
|
||||||
|
# TODO Remove if the integration does not have an options flow
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Remove if the integration does not have an options flow
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||||
|
entry, (Platform.SENSOR,)
|
||||||
|
):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""Config flow for NEW_NAME integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_ENTITY_ID
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
from homeassistant.helpers.helper_config_entry_flow import (
|
||||||
|
HelperConfigFlowHandler,
|
||||||
|
HelperFlowStep,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
OPTIONS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ENTITY_ID): selector.selector(
|
||||||
|
{"entity": {"domain": "sensor"}}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("name"): selector.selector({"text": {}}),
|
||||||
|
}
|
||||||
|
).extend(OPTIONS_SCHEMA.schema)
|
||||||
|
|
||||||
|
CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)}
|
||||||
|
|
||||||
|
OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||||
|
"""Handle a config or options flow for NEW_NAME."""
|
||||||
|
|
||||||
|
config_flow = CONFIG_FLOW
|
||||||
|
# TODO remove the options_flow if the integration does not have an options flow
|
||||||
|
options_flow = OPTIONS_FLOW
|
||||||
|
|
||||||
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
|
"""Return config entry title."""
|
||||||
|
return cast(str, options["name"]) if "name" in options else ""
|
|
@ -0,0 +1,3 @@
|
||||||
|
"""Constants for the NEW_NAME integration."""
|
||||||
|
|
||||||
|
DOMAIN = "NEW_DOMAIN"
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""Sensor platform for NEW_NAME integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize NEW_NAME config entry."""
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
# Validate + resolve entity registry id to entity_id
|
||||||
|
entity_id = er.async_validate_entity_id(
|
||||||
|
registry, config_entry.options[CONF_ENTITY_ID]
|
||||||
|
)
|
||||||
|
# TODO Optionally validate config entry options before creating entity
|
||||||
|
name = config_entry.title
|
||||||
|
unique_id = config_entry.entry_id
|
||||||
|
|
||||||
|
async_add_entities([NEW_DOMAINSensorEntity(unique_id, name, entity_id)])
|
||||||
|
|
||||||
|
|
||||||
|
class NEW_DOMAINSensorEntity(SensorEntity):
|
||||||
|
"""NEW_DOMAIN Sensor."""
|
||||||
|
|
||||||
|
def __init__(self, unique_id: str, name: str, wrapped_entity_id: str) -> None:
|
||||||
|
"""Initialize NEW_DOMAIN Sensor."""
|
||||||
|
super().__init__()
|
||||||
|
self._wrapped_entity_id = wrapped_entity_id
|
||||||
|
self._attr_name = name
|
||||||
|
self._attr_unique_id = unique_id
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""Test the NEW_NAME config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.NEW_DOMAIN.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("platform", ("sensor",))
|
||||||
|
async def test_config_flow(hass: HomeAssistant, platform) -> None:
|
||||||
|
"""Test the config flow."""
|
||||||
|
input_sensor_entity_id = "sensor.input"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.NEW_DOMAIN.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"name": "My NEW_DOMAIN", "entity_id": input_sensor_entity_id},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "My NEW_DOMAIN"
|
||||||
|
assert result["data"] == {}
|
||||||
|
assert result["options"] == {
|
||||||
|
"entity_id": input_sensor_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
assert config_entry.data == {}
|
||||||
|
assert config_entry.options == {
|
||||||
|
"entity_id": input_sensor_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
}
|
||||||
|
assert config_entry.title == "My NEW_DOMAIN"
|
||||||
|
|
||||||
|
|
||||||
|
def get_suggested(schema, key):
|
||||||
|
"""Get suggested value for key in voluptuous schema."""
|
||||||
|
for k in schema.keys():
|
||||||
|
if k == key:
|
||||||
|
if k.description is None or "suggested_value" not in k.description:
|
||||||
|
return None
|
||||||
|
return k.description["suggested_value"]
|
||||||
|
# Wanted key absent from schema
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("platform", ("sensor",))
|
||||||
|
async def test_options(hass: HomeAssistant, platform) -> None:
|
||||||
|
"""Test reconfiguring."""
|
||||||
|
input_sensor_1_entity_id = "sensor.input1"
|
||||||
|
input_sensor_2_entity_id = "sensor.input2"
|
||||||
|
|
||||||
|
# Setup the config entry
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
data={},
|
||||||
|
domain=DOMAIN,
|
||||||
|
options={
|
||||||
|
"entity_id": input_sensor_1_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
},
|
||||||
|
title="My NEW_DOMAIN",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
schema = result["data_schema"].schema
|
||||||
|
assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
"entity_id": input_sensor_2_entity_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"] == {
|
||||||
|
"entity_id": input_sensor_2_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
}
|
||||||
|
assert config_entry.data == {}
|
||||||
|
assert config_entry.options == {
|
||||||
|
"entity_id": input_sensor_2_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
}
|
||||||
|
assert config_entry.title == "My NEW_DOMAIN"
|
||||||
|
|
||||||
|
# Check config entry is reloaded with new options
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check the entity was updated, no new entity was created
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
# TODO Check the state of the entity has changed as expected
|
||||||
|
state = hass.states.get(f"{platform}.my_NEW_DOMAIN")
|
||||||
|
assert state.attributes == {}
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Test the NEW_NAME integration."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.NEW_DOMAIN.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("platform", ("sensor",))
|
||||||
|
async def test_setup_and_remove_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
platform: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting up and removing a config entry."""
|
||||||
|
input_sensor_entity_id = "sensor.input"
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
NEW_DOMAIN_entity_id = f"{platform}.my_NEW_DOMAIN"
|
||||||
|
|
||||||
|
# Setup the config entry
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
data={},
|
||||||
|
domain=DOMAIN,
|
||||||
|
options={
|
||||||
|
"entity_id": input_sensor_entity_id,
|
||||||
|
"name": "My NEW_DOMAIN",
|
||||||
|
},
|
||||||
|
title="My NEW_DOMAIN",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check the entity is registered in the entity registry
|
||||||
|
assert registry.async_get(NEW_DOMAIN_entity_id) is not None
|
||||||
|
|
||||||
|
# Check the platform is setup correctly
|
||||||
|
state = hass.states.get(NEW_DOMAIN_entity_id)
|
||||||
|
# TODO Check the state of the entity has changed as expected
|
||||||
|
assert state.state == "unknown"
|
||||||
|
assert state.attributes == {}
|
||||||
|
|
||||||
|
# Remove the config entry
|
||||||
|
assert await hass.config_entries.async_remove(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check the state and entity registry entry are removed
|
||||||
|
assert hass.states.get(NEW_DOMAIN_entity_id) is None
|
||||||
|
assert registry.async_get(NEW_DOMAIN_entity_id) is None
|
Loading…
Add table
Reference in a new issue