Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9e97e44
RFC commit
Kane610 Dec 2, 2018
cf3cb13
Stash changes
Kane610 Jan 12, 2019
d2ad5f5
Puzzle pieces
Kane610 Jan 22, 2019
b957ae7
Getting closer
Kane610 Feb 2, 2019
45281fd
Remove debug stuff
Kane610 Feb 8, 2019
7dd451b
Fix up following suggestions from Balloob
Kane610 Feb 8, 2019
f80e8b8
Use similar implementation to update callbacks as entity registry
Kane610 Feb 10, 2019
2da24e0
Updated text and usage example
Kane610 Feb 10, 2019
ae954e9
Type hints
Kane610 Feb 10, 2019
3b907a5
Add tests for config entry options
Kane610 Feb 12, 2019
f9b1446
Nearly there?
Kane610 Feb 13, 2019
f5aa8fd
Clean up
Kane610 Feb 13, 2019
2f57642
Remove stale prints
Kane610 Feb 13, 2019
bf06d3e
Fix hound comments
Kane610 Feb 13, 2019
27d4679
Fix empty docstring in test
Kane610 Feb 15, 2019
1b1cec1
Fix typing
Kane610 Feb 15, 2019
dc82dc1
Fix line too long
Kane610 Feb 15, 2019
c0f4e43
RFC commit
Kane610 Dec 2, 2018
b91b5c8
Stash changes
Kane610 Jan 12, 2019
c5403d6
Puzzle pieces
Kane610 Jan 22, 2019
d0a51fb
Getting closer
Kane610 Feb 2, 2019
c2a42a9
Remove debug stuff
Kane610 Feb 8, 2019
97a8b9e
Fix up following suggestions from Balloob
Kane610 Feb 8, 2019
aad7a6a
Use similar implementation to update callbacks as entity registry
Kane610 Feb 10, 2019
e8aa1b2
Updated text and usage example
Kane610 Feb 10, 2019
296f5ae
Type hints
Kane610 Feb 10, 2019
ccf95e3
Add tests for config entry options
Kane610 Feb 12, 2019
191a603
Nearly there?
Kane610 Feb 13, 2019
51c8acf
Clean up
Kane610 Feb 13, 2019
a95346b
Remove stale prints
Kane610 Feb 13, 2019
9561eec
Fix hound comments
Kane610 Feb 13, 2019
4eff880
Fix empty docstring in test
Kane610 Feb 15, 2019
8c925a2
Fix typing
Kane610 Feb 15, 2019
16f712a
Fix line too long
Kane610 Feb 15, 2019
2020edf
No 3.6 typing and ignore type issue instead
Kane610 Feb 15, 2019
37205ce
Merge branch 'config-entry-options' of github.com:Kane610/home-assist…
Kane610 Feb 15, 2019
2d36169
Permission should be edit and not add
Kane610 Feb 15, 2019
728c7a4
Define type in comment
Kane610 Feb 15, 2019
8252117
Inline supports_options
Kane610 Feb 15, 2019
baae873
Fix tests
Kane610 Feb 18, 2019
b233b6b
Options shouldn't be None
Kane610 Feb 20, 2019
a7bf0a2
Rename Options to OptionsFlowManager
Kane610 Feb 20, 2019
4179a4b
Options flow doesn't care about context
Kane610 Feb 20, 2019
1206207
Remove stale print
Kane610 Feb 20, 2019
a07d851
Fix tests
Kane610 Feb 20, 2019
942d4c7
Martin is right
Kane610 Feb 20, 2019
9c2ae55
Fix typing issues
Kane610 Feb 21, 2019
ccf2b15
Update added comment in code
Kane610 Feb 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion homeassistant/components/config/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ async def async_setup(hass):
hass.http.register_view(
ConfigManagerFlowResourceView(hass.config_entries.flow))
hass.http.register_view(ConfigManagerAvailableFlowView)
hass.http.register_view(
OptionManagerFlowIndexView(hass.config_entries.options.flow))
hass.http.register_view(
OptionManagerFlowResourceView(hass.config_entries.options.flow))
return True


Expand Down Expand Up @@ -45,15 +49,19 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
name = 'api:config:config_entries:entry'

async def get(self, request):
"""List flows in progress."""
"""List available config entries."""
hass = request.app['hass']

return self.json([{
'entry_id': entry.entry_id,
'domain': entry.domain,
'title': entry.title,
'source': entry.source,
'state': entry.state,
'connection_class': entry.connection_class,
'supports_options': hasattr(
config_entries.HANDLERS[entry.domain],
'async_get_options_flow'),
} for entry in hass.config_entries.async_entries()])


Expand Down Expand Up @@ -145,3 +153,48 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request):
"""List available flow handlers."""
return self.json(config_entries.FLOWS)


class OptionManagerFlowIndexView(FlowManagerIndexView):
Comment thread
Kane610 marked this conversation as resolved.
"""View to create option flows."""

url = '/api/config/config_entries/entry/option/flow'
name = 'api:config:config_entries:entry:resource:option:flow'

# pylint: disable=arguments-differ
async def post(self, request):
"""Handle a POST request.

handler in request is entry_id.
"""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='edit')

# pylint: disable=no-value-for-parameter
return await super().post(request)


class OptionManagerFlowResourceView(ConfigManagerFlowResourceView):
"""View to interact with the option flow manager."""

url = '/api/config/config_entries/options/flow/{flow_id}'
name = 'api:config:config_entries:options:flow:resource'

async def get(self, request, flow_id):
"""Get the current state of a data_entry_flow."""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='edit')

return await super().get(request, flow_id)

# pylint: disable=arguments-differ
async def post(self, request, flow_id):
"""Handle a POST request."""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='edit')

# pylint: disable=no-value-for-parameter
return await super().post(request, flow_id)
93 changes: 85 additions & 8 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ async def async_step_discovery(info):
import logging
import functools
import uuid
from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import
from typing import Callable, Dict, List, Optional, Set # noqa pylint: disable=unused-import
import weakref

from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady
from homeassistant.setup import async_setup_component, async_process_deps_reqs
from homeassistant.util.decorator import Registry


_LOGGER = logging.getLogger(__name__)
_UNDEF = object()

Expand Down Expand Up @@ -221,12 +221,13 @@ async def async_step_discovery(info):
class ConfigEntry:
"""Hold a configuration entry."""

__slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source',
'connection_class', 'state', '_setup_lock',
'_async_cancel_retry_setup')
__slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options',
'source', 'connection_class', 'state', '_setup_lock',
'update_listeners', '_async_cancel_retry_setup')

def __init__(self, version: str, domain: str, title: str, data: dict,
source: str, connection_class: str,
options: Optional[dict] = None,
entry_id: Optional[str] = None,
state: str = ENTRY_STATE_NOT_LOADED) -> None:
"""Initialize a config entry."""
Expand All @@ -245,6 +246,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict,
# Config data
self.data = data

# Entry options
self.options = options or {}

# Source of the configuration (user, discovery, cloud)
self.source = source

Expand All @@ -254,6 +258,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict,
# State of the entry (LOADED, NOT_LOADED)
self.state = state

# Listeners to call on update
self.update_listeners = [] # type: list

# Function to cancel a scheduled retry
self._async_cancel_retry_setup = None

Expand Down Expand Up @@ -384,6 +391,18 @@ async def async_migrate(self, hass: HomeAssistant) -> bool:
self.title, component.DOMAIN)
return False

def add_update_listener(self, listener: Callable) -> Callable:
"""Listen for when entry is updated.

Listener: Callback function(hass, entry)

Returns function to unlisten.
"""
weak_listener = weakref.ref(listener)
self.update_listeners.append(weak_listener)

return lambda: self.update_listeners.remove(weak_listener)

def as_dict(self):
"""Return dictionary version of this entry."""
return {
Expand All @@ -392,6 +411,7 @@ def as_dict(self):
'domain': self.domain,
'title': self.title,
'data': self.data,
'options': self.options,
'source': self.source,
'connection_class': self.connection_class,
}
Expand All @@ -416,6 +436,7 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None:
self.hass = hass
self.flow = data_entry_flow.FlowManager(
hass, self._async_create_flow, self._async_finish_flow)
self.options = OptionsFlowManager(hass)
self._hass_config = hass_config
self._entries = [] # type: List[ConfigEntry]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
Expand All @@ -433,6 +454,14 @@ def async_domains(self) -> List[str]:

return result

@callback
def async_get_entry(self, entry_id: str) -> Optional[ConfigEntry]:
"""Return entry with matching entry_id."""
for entry in self._entries:
if entry_id == entry.entry_id:
return entry
return None

@callback
def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]:
"""Return all entries or entries for a specific domain."""
Expand Down Expand Up @@ -490,14 +519,25 @@ async def async_load(self) -> None:
title=entry['title'],
# New in 0.79
connection_class=entry.get('connection_class',
CONN_CLASS_UNKNOWN))
CONN_CLASS_UNKNOWN),
# New in 0.89
options=entry.get('options'))
for entry in config['entries']]

@callback
def async_update_entry(self, entry, *, data=_UNDEF):
def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF):
"""Update a config entry."""
if data is not _UNDEF:
entry.data = data

if options is not _UNDEF:
entry.options = options
Comment thread
Kane610 marked this conversation as resolved.

if data is not _UNDEF or options is not _UNDEF:
for listener_ref in entry.update_listeners:
listener = listener_ref()
self.hass.async_create_task(listener(self.hass, entry))

self._async_schedule_save()

async def async_forward_entry_setup(self, entry, component):
Expand Down Expand Up @@ -547,6 +587,7 @@ async def _async_finish_flow(self, flow, result):
domain=result['handler'],
title=result['title'],
data=result['data'],
options={},
Comment thread
MartinHjelmare marked this conversation as resolved.
source=flow.context['source'],
connection_class=flow.CONNECTION_CLASS,
)
Expand Down Expand Up @@ -596,7 +637,7 @@ async def _async_create_flow(self, handler_key, *, context, data):
flow.init_step = source
return flow

def _async_schedule_save(self):
def _async_schedule_save(self) -> None:
"""Save the entity registry to a file."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)

Expand Down Expand Up @@ -629,3 +670,39 @@ def _async_in_progress(self):
return [flw for flw in self.hass.config_entries.flow.async_progress()
if flw['handler'] == self.handler and
flw['flow_id'] != self.flow_id]


class OptionsFlowManager:
"""Flow to set options for a configuration entry."""

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the options manager."""
self.hass = hass
Comment thread
Kane610 marked this conversation as resolved.
self.flow = data_entry_flow.FlowManager(
hass, self._async_create_flow, self._async_finish_flow)

async def _async_create_flow(self, entry_id, *, context, data):
"""Create an options flow for a config entry.

Entry_id and flow.handler is the same thing to map entry with flow.
"""
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
return
flow = HANDLERS[entry.domain].async_get_options_flow(
entry.data, entry.options)
return flow

async def _async_finish_flow(self, flow, result):
"""Finish an options flow and update options for configuration entry.

Flow.handler and entry_id is the same thing to map flow with entry.
"""
entry = self.hass.config_entries.async_get_entry(flow.handler)
if entry is None:
return
self.hass.config_entries.async_update_entry(
entry, options=result['data'])

result['result'] = True
return result
3 changes: 2 additions & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,13 +608,14 @@ class MockConfigEntry(config_entries.ConfigEntry):

def __init__(self, *, domain='test', data=None, version=1, entry_id=None,
source=config_entries.SOURCE_USER, title='Mock Title',
state=None,
state=None, options={},
connection_class=config_entries.CONN_CLASS_UNKNOWN):
"""Initialize a mock config entry."""
kwargs = {
'entry_id': entry_id or 'mock-id',
'domain': domain,
'data': data or {},
'options': options,
'version': version,
'title': title,
'connection_class': connection_class,
Expand Down
Loading