-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Config Manager #11947
Comments
This is definitely needed to get hassio to reach it's goals. A couple comments and questions:
I am definitely up for helping with the grunt work of adding handlers to components. Per my last point, this would be a lot of modifications (ala lazy service loading) but on a bigger scale. Hmm, on second thought let's make @amelchio do it! |
My bad on the account part, that is indeed confusing. I started out naming it account manager but then at the end decided it should be a more generic "Config Manager". I forgot to update most examples. Will do so now. File will be stored as a single YAML file and kept in memory afterwards. This is not targeted at Hass.io. This is meant for components inside Home Assistant. Hass.io needs to figure out their own patterns. Discussion here I am a UI person. The idea is to be able to generate forms from voluptuous schemas. Handlers will live in each component. We don't have to "upgrade" everything. We can just upgrade as we go. Once we have done a couple different variations, we can see if the API is solid or not. I wouldn't want to scan for handlers in the source, as it requires each file to be loaded into memory and Python doesn't forget. Instead I was thinking about initially just hardcoding the list (Hue, Cast, Wemo) and later we can migrate to something similar to |
This looks great! Will you allow modification of already configured component or would it require a remove + add? |
Initially remove + add. Edit can be added later. |
From a file system perspective, it would be good to create a sub-directory for all these configuration files to live in. This will stop the top level folder becoming (even more) horribly cluttered with lots of component configuration files. |
^would also make the .gitignore file a little easier to make sure you don't upload the faff. Just be careful to warn people in case they already have a folder by that name so they can rename it before hand. |
I'm glad to see this being worked on! This is a great advance for HA! |
Initially we would focus on storing account info in the config manager. In that case it is all secrets. What people like to share is automations, which for now will not be part of this. |
Finally got a chance to read this, some notes:
import voluptuous as vol
from homeassistant.helpers import config_manager
@config_manager.config_handler('nest')
class NestConfig(config_manager.ConfigBase):
# Future versions of config might require a migration
VERSION = 1
async def async_handle(self, step_number, user_input):
"""
Handle a step in the add config process.
step_number: the step that we're on.
user_input: input received from user for the step.
If user_input is None, return form for that step.
"""
if step_number == 0:
return self.show_form(
step_number=0,
total_steps=2,
title='Client information',
introduction='Some Markdown introduction',
schema=vol.Schema({
# Translation keys are:
# components.<comp name>.create_config.<step>.<key name>.caption
# components.<comp name>.create_config.<step>.<key name>.description
# In this case: components.nest.create_config.0.client_id.caption
vol.Required('client_id'): cv.string,
vol.Required('client_secret'): cv.string,
})
)
if step_number == 1:
self.client_data = user_input
return self.show_form(
step_number=1,
total_steps=2,
title='Authorize account',
introduction='Markdown link to oauth flow',
schema=Vol.Schema({
vol.Required('auth_code')
})
)
if step_number == 2:
import nest
# Validate auth code and get access token
token_info = await nest.async_get_credentials(
self.client_data['client_id'],
self.client_data['client_secret'],
user_input['auth_code'])
user = await nest.async_get_user(token_info['access_token'])
return self.create_config(
title=user['name'],
data=token_info
) |
Also, another use case to consider. The Automatic device tracker. Currently, after specifying the client id / client secret, we direct the user to an OAuth URL. When the user is authenticated, Automatic redirects them back to home assistant, and the required key is posted with that redirect and saved internally by hass, requiring no further action from the user, as opposed to the nest case where a key needs to be copied/pasted. |
All great points of course from armills. Couple thoughts on steps. Rather than directly declaring step_numbers this would seem to best be abstracted by const declarations and a next_step and maybe last_step which would allow us to more easily add steps. Within the context we know what steps we need. But if an app required/supported oauth for instance, we could query what steps are needed at the beginning and show appropriate text at the start of the config, ie. don't let them keep failing oauth if they don't have the correct config to support CONFIG_MGR_OAUTH_INIT. Would also give some definition to what each step was meant to accomplish within the code. As to armills point on translation, these could probably be used as a basis for a generic translation for form introduction/description. With field specific descriptions/errors this would seem to cover most of that need. CONSTS
NEST
HUE
CAST
|
Good point about the steps. Your code is for sure cleaner. Let me try to clarify my reasoning a bit. The The Besides DISCOVERY I also expect CLOUD to be a possible "step" for configuration. Take your Automatic example:
Another potential Yes about errors, translations. create_config would return an object that represents the action. It's just a helper to make sure we include flow_id etc. The actual processing of the action happens in ConfigManager class, not ConfigHandler. Decorator is to register the ConfigHandler so we know which ConfigHandlers are available. Otherwise all components will need to call it the same thing (as we do for I also thought we would use the decorator for discoverability but realized that it won't show up unless that file is loaded, and we're not going to load all components. Instead will have to end up generating a list of components that support the Config Manager. Yes to search and description. |
Actually wonder if description is even necessary? It's going to be replaced by translation anyway. (not sure how we should do that with markdown though, in case we want to include link/image) |
Ugh let me lazily retype this after some errant keystroke turned this into a blank page... So I think functions should accept an error class that can contain the step and fields causing errors (since vol.form can't handle all cases). Show form already accepts it but this would allow us to retry the form.
I think with how much we have to do with translations, we should just make sure we aren't backed into a corner but shouldn't worry beyond that (much). Abstracting these errors and steps to a meaningful const will allow us to translate it at the frontend. Adding beyond that means fundamentally adding a lang global and all that. With this we could have the frontend display hass.translation['ERROR_CONFIG_MGR_OAUTH_INIT'] down the road. |
Ah, I think I see where you're going with the import voluptuous as vol
from homeassistant.helpers import config_manager
STEP_CLIENT_INFO = 'client_info'
STEP_OAUTH = 'oauth'
@config_manager.config_handler('nest')
class NestConfig(config_manager.ConfigBase):
# Future versions of config might require a migration
VERSION = 1
def __init__(self):
# Data from step 0 that we'll store.
self.client_data = None
async def async_handle(self, source_step_id, user_input):
"""
Handle a step in the add config process.
source_step_id: the step that originated the request.
user_input: input received from user for the step.
If user_input is None, return form for that step.
"""
if source_step_id is None: # Or whatever core constant gets used
return self.show_form(
step_id=STEP_CLIENT_INFO
step_number=1,
total_steps=2,
title='Client information',
introduction='Some Markdown introduction',
schema=vol.Schema({
# Translation keys are:
# components.<comp name>.create_config.<step>.<key name>.caption
# components.<comp name>.create_config.<step>.<key name>.description
# In this case: components.nest.create_config.0.client_id.caption
vol.Required('client_id'): cv.string,
vol.Required('client_secret'): cv.string,
})
)
if source_step_id is STEP_CLIENT_INFO:
self.client_data = user_input
return self.show_form(
step_id=STEP_OAUTH,
step_number=2,
total_steps=2,
title='Authorize account',
introduction='Markdown link to oauth flow',
schema=Vol.Schema({
vol.Required('auth_code')
})
)
if source_step_id is STEP_OAUTH:
import nest
# Validate auth code and get access token
token_info = await nest.async_get_credentials(
self.client_data['client_id'],
self.client_data['client_secret'],
user_input['auth_code'])
user = await nest.async_get_user(token_info['access_token'])
return self.create_config(
title=user['name'],
data=token_info
) |
@armills how would we do error handling in that case? if source_step_id is STEP_CLIENT_INFO:
is_valid = yield from validate_client_info(user_input)
if not is_valid:
# We want to show form for STEP_CLIENT_INFO I think that getting the data back to the step that rendered the form is the most clear. It makes no assumptions and the current step can decide what is next (with steps based on the just given input). Btw just had a thought about translations. For a lot of things we should be able to get standardized translations (username, password, email etc) |
Just had an insight. What if each step got its own method, dynamically called. @asyncio.coroutine
def async_handle_client_info(self, user_input=None):
@asyncio.coroutine
def async_handle_discovery(self, input):
@asyncio.coroutine
def async_handle_oauth(self, input): We would still do the redirect stuff of returning another step. |
I put out a prototype of this RFC up in #12079. Have been hacking at it for a couple of days. Have taken most comments into consideration. Nothing is set in stone yet but since there are so many moving pieces, I wanted to code some things together so I can start looking at it from the frontend point. |
I think the routing would certainly be easier to read if they were named methods. In either case platforms could always create their own internal methods to return the Here's another idea for brainstorming: What if these configuration methods were coroutines that returned the user input once it's received? This would let us take advantage of async magic to write the routing rules using normal flow control patterns, which might be easier to grok for most contributors. Example: import voluptuous as vol
from homeassistant.helpers import config_manager
@config_manager.config_handler('nest')
class NestConfig(config_manager.ConfigBase):
# Future versions of config might require a migration
VERSION = 1
async def async_handle(self, discovery_data=None):
"""
Handle a step in the add config process.
discovery_data: Additional information if this was initiated by discovery
"""
client_data = None
while client_data is None:
user_input = await self.show_form(
step_id=STEP_CLIENT_INFO
step_number=1,
total_steps=2,
title='Client information',
introduction='Some Markdown introduction',
schema=vol.Schema({
# Translation keys are:
# components.<comp name>.create_config.<step>.<key name>.caption
# components.<comp name>.create_config.<step>.<key name>.description
# In this case: components.nest.create_config.0.client_id.caption
vol.Required('client_id'): cv.string,
vol.Required('client_secret'): cv.string,
})
)
if is_valid(user_input):
client_data = user_input
user = None
while auth_code is None:
user_input = await self.show_form(
step_id=STEP_OAUTH,
step_number=2,
total_steps=2,
title='Authorize account',
introduction='Markdown link to oauth flow',
schema=Vol.Schema({
vol.Required('auth_code')
})
)
import nest
# Validate auth code and get access token
token_info = await nest.async_get_credentials(
client_data['client_id'],
client_data['client_secret'],
user_input['auth_code'])
user = await nest.async_get_user(token_info['access_token'])
# This could possibly also be a coroutine if we wanted to allow final
# cleanup afterwards, although it's probably best to enforce
# create_config as the termination of async_handle
return self.create_config(
title=user['name'],
data=token_info
) |
I thought more about putting in 1 method but decided I don't like it. Main downside is that based on a step, we might want to show different config options. It gets pretty confusing code if that is all tied together with if…else commands. |
One other thought I had while riding, is adding a property in the config manager for 'created_by' or 'owner' with values like discovery, user, component_x. It is a bit down the road, but the thought being that if a component created several entities in the config_manager and was subsequently removed it would allow us to track other entities that depended on it. |
Demo video of the config manager in action Hue works completely: create entry -> loads Hue. Restart will load entry again. Backend PR: #12079 |
The code is finished now for both frontend and backend PR (wrapping up tests as we speak) I've added an experimental and temporary component The functionality will not be exposed to users yet. To experiment with it, update your config: config:
config_entries: true The reason I am this cautious is that we need more experimentation to see if the API needs more tweaking. |
Here is an architecture diagram of how it all works together: https://docs.google.com/drawings/d/10ESEPcVplCzcBg7_Ir4bpgka5YjVbtBRnsebetYkpxA/edit?usp=sharing |
Here is a video of the config manager in action https://youtu.be/_qpKe-FW9Yk |
@c727 the idea is that entities will know to which config entry they belong. So then we can show those together. |
Sorry, long RFC
Goal: allow people to configure components from the frontend without having to touch any text file.
Subgoal: have discovery use this system so people can disable certain things from being discovered again or configure discovered items.
TL;DR: New helper config_manager. Components can declare config handlers that help in creation of config data. The helper will store the config data outside of
configuration.yaml
. HASS will setup known config pieces before processing normal config.So I've been thinking about a config manager to power a config UI for Home Assistant. For a long time I thought that a config manager for Home Assistant should be build on top of
configuration.yaml
. But after thinking a bit more, I realized that it's going to make things too complicated and messy. Especially as configuration.yaml has been overly complicated with packages,!include
etc etc.I've approached the design by looking at the following component configuration use cases:
client_id
,client_secret
. Second step requires auth linking by following a link to the Nest.com website and copying a pin code into Home Assistant.Scope of this RFC:
Out of scope for MVP:
My idea is to achieve this with a new helper:
homeassistant.helpers.config_manager
. It will allow components to register config handlers by extending theConfigBase
class.Hass will:
configuration.yaml
. This will allow things like entity IDs to exist before automations etc are loaded. Setting up components with stored configs is done with two new methods:Not 100% yet what else we want to pass to these methods. Maybe a component config (also stored in config manager, not from
configuration.yaml
)homeassistant.helpers.config_manager
is responsible for:Config handlers (like
HueConfigHandler
) are responsible for:When handling the user input, the config handler can choose to:
Time for some code
I've been tweaking design by writing code and see if it would make sense. Here is code that would work for Nest, Hue and Chromecast.
1. Nest
The Nest component requires a 2 step configuration. First step is to add
client_id
,client_secret
. Second step requires auth linking by following a link to the Nest.com website and copying a pin code into Home Assistant.2. Hue
Hue bridge is automatically discovered. After discovery, the user has to press a button on the bridge to authorize Home Assistant to create a user in the next 30 seconds.
3. Chromecast
Chromecast is automatically discovered and does not require authorization.
Migrations
Migration support would be easy to add. Each config handler has a VERSION const that we store together with the config entry. When we are going to setup components with stored config, we can see if the stored version is the same as the current config handler version.
If not, call a migration method on the config handler:
Discovery
Discovery entries will go to the config handler. If the config handler generates a form to show, the frontend will be notified (TBD).
When an config entry is created from discovery, we should set the source to discovery. That way we can indicate to the user that it was automatically discovered and that it should disable, not remove the config entry if it doesn't want to see it in Home Assistant anymore. By disabling, it will exist but not be loaded. If deleted, a new run of discovery will just add it again.
Notes
Full stack:
Some interaction examples from user interaction to interacting with the config handler inside a component.
All interactions are done via a new section in the config panel. It will list all current configuration entries and has an "Add Integration" button. Because entities contain the config entry that they are part of as part of their attributes, we can show the entities that are part of each config entry.
Adding an config entry
/api/config/types
/api/config/entry
. This will create a temporary entry id. Under the hood the config manager will instantiate an instance ofNestConfigHandler
and callsNestConfigHandler.async_handle(0)
. This instance is kept for the duration of the config process so that the backend can track state./api/config/entry/<id>
NestConfigHandler.async_handle(0, user_input)
which returns a new form to show./api/config/entries/<id>
NestConfigHandler.async_handle(1, user_input)
which returnsself.create_config
. The config manager will store this config entry, callhomeassistant.components.nest.async_setup_config(hass, config_info)
and return "form done" command.Removing a config entry
/api/config/entries/
and show it as list to the user.DELETE /api/config/entries<id>
homeassistant.components.nest.async_unload_config(hass, config_info)
if available.async_unload_config
did not exist, show a message that the user has to restart Home Assistant for the integration to be unloaded.The text was updated successfully, but these errors were encountered: