From 9e97e44eda67e3e36d3148b09184398116fd1028 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 2 Dec 2018 18:07:06 +0100 Subject: [PATCH 01/47] RFC commit --- .../components/deconz/config_flow.py | 22 +++++ homeassistant/config_entries.py | 92 ++++++++++++++++++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 8f90f303fcaad..0fa72ea6201ba 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -168,3 +168,25 @@ async def async_step_import(self, import_config): user_input = {CONF_ALLOW_CLIP_SENSOR: True, CONF_ALLOW_DECONZ_GROUPS: True} return await self.async_step_options(user_input=user_input) + + @staticmethod + def async_get_options_flow(self): + """""" + return DeconzOptionsFlowHandler + + +from homeassistant import data_entry_flow + + +class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): + """Handle a deCONZ options flow.""" + + VERSION = 1 + + def __init__(self): + """""" + pass + + def async_step_user(self, user_input=None): + """""" + pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index cb79f457ce57b..df7e36971fb77 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -221,12 +221,12 @@ 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', + __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', + 'source', 'connection_class', 'state', '_setup_lock', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, + source: str, connection_class: str, options: dict = {}, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -245,6 +245,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict, # Config data self.data = data + # Entry options + self.options = options + # Source of the configuration (user, discovery, cloud) self.source = source @@ -392,6 +395,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, } @@ -416,6 +420,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 = Options(hass) self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -475,6 +480,7 @@ async def async_load(self) -> None: self.hass.config.path(PATH_CONFIG), self._store, old_conf_migrate_func=_old_conf_migrator ) + print('async load config entries', config) if config is None: self._entries = [] @@ -490,14 +496,29 @@ 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.8x + 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=None): """Update a config entry.""" + if not data and not options: + return + if data is not _UNDEF: entry.data = data + + if options: + entry.options = options + + component = getattr(self.hass.components, entry.domain) + dynamic_options = hasattr(component, 'async_options_updated') + if dynamic_options: + self.hass.async_create_task( + component.async_options_updated(self.hass, entry)) + self._async_schedule_save() async def async_forward_entry_setup(self, entry, component): @@ -629,3 +650,64 @@ 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] + + @staticmethod + def async_get_options_flow(): + """""" + return None + + +class Options: + """""" + + def __init__(self, hass: HomeAssistant) -> None: + """""" + self.hass = hass + self.flow = data_entry_flow.FlowManager( + hass, self._async_create_flow, self._async_finish_flow) + self.active_options = {} + + @callback + def async_domains(self) -> List[str]: + """Return domains for which we have options.""" + result = [] + + for domain in FLOWS: + if HANDLERS[domain].async_get_options_flow(): + result.append(domain) + + return result + + @callback + def async_active_flow(self, entry: ConfigEntry): + """""" + if entry in self.active_options.values(): + return True + return False + + + @callback + def _async_create_flow(self, handler, context, data: ConfigEntry): + """""" + handler = HANDLERS[handler].async_get_options_flow() + flow = handler(data['data']) + flow.init_step = context['source'] + self.active_options[flow.flow_id] = data + return flow + + @callback + def _async_finish_flow(self, flow, result): + """""" + entry = self.active_options.pop(flow.flow_id) + self.hass.config_entries.async_update_entry( + entry, options=result['data']) + return result + + +class OptionsFlow(data_entry_flow.FlowHandler): + """""" + + @property + def data(self): + """""" + return self._config_entry['data'] From cf3cb1368d0474b655fffe2c7910119c1e64a391 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 12 Jan 2019 10:15:50 +0100 Subject: [PATCH 02/47] Stash changes --- .../components/deconz/config_flow.py | 2 +- homeassistant/config_entries.py | 23 +------------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 0fa72ea6201ba..d3603aed458cb 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -187,6 +187,6 @@ def __init__(self): """""" pass - def async_step_user(self, user_input=None): + def async_step_init(self, user_input=None): """""" pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index df7e36971fb77..5d8d5eb0d7fd0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,7 @@ class ConfigEntry: '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, options: dict = {}, + source: str, connection_class: str, options: dict = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -667,17 +667,6 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def async_domains(self) -> List[str]: - """Return domains for which we have options.""" - result = [] - - for domain in FLOWS: - if HANDLERS[domain].async_get_options_flow(): - result.append(domain) - - return result - @callback def async_active_flow(self, entry: ConfigEntry): """""" @@ -685,7 +674,6 @@ def async_active_flow(self, entry: ConfigEntry): return True return False - @callback def _async_create_flow(self, handler, context, data: ConfigEntry): """""" @@ -702,12 +690,3 @@ def _async_finish_flow(self, flow, result): self.hass.config_entries.async_update_entry( entry, options=result['data']) return result - - -class OptionsFlow(data_entry_flow.FlowHandler): - """""" - - @property - def data(self): - """""" - return self._config_entry['data'] From d2ad5f5526cb137ea53635e30cb6f8a00b989543 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Jan 2019 21:13:10 +0100 Subject: [PATCH 03/47] Puzzle pieces --- homeassistant/components/deconz/__init__.py | 5 +++++ homeassistant/components/deconz/config_flow.py | 2 +- homeassistant/components/deconz/strings.json | 6 ++++++ homeassistant/config_entries.py | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8015324be13fd..69fb988b2c2f0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -161,6 +161,11 @@ async def async_refresh_devices(call): return True +async def async_options_updated(hass, config_entry): + """Options have changed.""" + pass + + async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" gateway = hass.data.pop(DOMAIN) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d3603aed458cb..68a26c6a17934 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -183,7 +183,7 @@ class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): VERSION = 1 - def __init__(self): + def __init__(self, config, options): """""" pass diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 1bf7235713afb..4ec5a6fff70f9 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -29,5 +29,11 @@ "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } + }, + "options": { + "title": "Options for deCONZ Zigbee gateway", + "step": {}, + "error": {}, + "abort": {} } } \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5d8d5eb0d7fd0..013a415c8a017 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -568,6 +568,7 @@ async def _async_finish_flow(self, flow, result): domain=result['handler'], title=result['title'], data=result['data'], + options=None, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -678,7 +679,7 @@ def async_active_flow(self, entry: ConfigEntry): def _async_create_flow(self, handler, context, data: ConfigEntry): """""" handler = HANDLERS[handler].async_get_options_flow() - flow = handler(data['data']) + flow = handler(data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data return flow From b957ae777ed232787a3465921e7b7973e60a4f04 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 2 Feb 2019 12:17:37 +0100 Subject: [PATCH 04/47] Getting closer --- .../components/config/config_entries.py | 36 +++++++++++++++++++ .../components/deconz/config_flow.py | 2 +- homeassistant/config_entries.py | 4 +-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 68890a79ca653..f1ec8d8fabc6a 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -17,6 +17,8 @@ async def async_setup(hass): hass.http.register_view( ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) + hass.http.register_view( + OptionsManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -47,6 +49,14 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request): """List flows in progress.""" hass = request.app['hass'] + + def supports_options(domain): + """""" + if domain == 'axis': + return None + handler = config_entries.HANDLERS[domain] + return handler.async_get_options_flow() is not None + return self.json([{ 'entry_id': entry.entry_id, 'domain': entry.domain, @@ -54,6 +64,7 @@ async def get(self, request): 'source': entry.source, 'state': entry.state, 'connection_class': entry.connection_class, + 'supports_options': supports_options(entry.domain), } for entry in hass.config_entries.async_entries()]) @@ -145,3 +156,28 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) + + +class OptionsManagerFlowResourceView(FlowManagerResourceView): + """View to interact with the options 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='add') + + 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='add') + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 68a26c6a17934..27d31580eb366 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -170,7 +170,7 @@ async def async_step_import(self, import_config): return await self.async_step_options(user_input=user_input) @staticmethod - def async_get_options_flow(self): + def async_get_options_flow(): """""" return DeconzOptionsFlowHandler diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 013a415c8a017..90d00887f364c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -676,9 +676,9 @@ def async_active_flow(self, entry: ConfigEntry): return False @callback - def _async_create_flow(self, handler, context, data: ConfigEntry): + def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): """""" - handler = HANDLERS[handler].async_get_options_flow() + handler = HANDLERS[handler_key].async_get_options_flow() flow = handler(data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data From 45281fd2a99adc29e3e4cd6fe46b3b45ff329e8f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Feb 2019 17:31:18 +0100 Subject: [PATCH 05/47] Remove debug stuff --- homeassistant/components/config/config_entries.py | 2 -- homeassistant/components/deconz/config_flow.py | 2 -- homeassistant/config_entries.py | 1 - 3 files changed, 5 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index f1ec8d8fabc6a..7d1c034b3fc13 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -52,8 +52,6 @@ async def get(self, request): def supports_options(domain): """""" - if domain == 'axis': - return None handler = config_entries.HANDLERS[domain] return handler.async_get_options_flow() is not None diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 27d31580eb366..3b40984293b13 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -181,8 +181,6 @@ def async_get_options_flow(): class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): """Handle a deCONZ options flow.""" - VERSION = 1 - def __init__(self, config, options): """""" pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 90d00887f364c..2c33a83d2dd31 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -480,7 +480,6 @@ async def async_load(self) -> None: self.hass.config.path(PATH_CONFIG), self._store, old_conf_migrate_func=_old_conf_migrator ) - print('async load config entries', config) if config is None: self._entries = [] From 7dd451b7292425de34619c94851c6374c8c9db14 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Feb 2019 00:05:59 +0100 Subject: [PATCH 06/47] Fix up following suggestions from Balloob --- .../components/config/config_entries.py | 4 ++-- homeassistant/components/deconz/config_flow.py | 4 ++-- homeassistant/config_entries.py | 16 ++-------------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 7d1c034b3fc13..746b12571b287 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -52,8 +52,8 @@ async def get(self, request): def supports_options(domain): """""" - handler = config_entries.HANDLERS[domain] - return handler.async_get_options_flow() is not None + return hasattr( + config_entries.HANDLERS[domain], 'async_get_options_flow') return self.json([{ 'entry_id': entry.entry_id, diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 3b40984293b13..741dc7a141e6b 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -170,9 +170,9 @@ async def async_step_import(self, import_config): return await self.async_step_options(user_input=user_input) @staticmethod - def async_get_options_flow(): + def async_get_options_flow(config, options): """""" - return DeconzOptionsFlowHandler + return DeconzOptionsFlowHandler(config, options) from homeassistant import data_entry_flow diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2c33a83d2dd31..a708df320a1eb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -651,11 +651,6 @@ def _async_in_progress(self): if flw['handler'] == self.handler and flw['flow_id'] != self.flow_id] - @staticmethod - def async_get_options_flow(): - """""" - return None - class Options: """""" @@ -667,18 +662,11 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def async_active_flow(self, entry: ConfigEntry): - """""" - if entry in self.active_options.values(): - return True - return False - @callback def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): """""" - handler = HANDLERS[handler_key].async_get_options_flow() - flow = handler(data['data'], data['options']) + flow = HANDLERS[handler_key].async_get_options_flow( + data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data return flow From f80e8b8efd7ea87cf63db340bf54e55f033ecfc1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 17:08:49 +0100 Subject: [PATCH 07/47] Use similar implementation to update callbacks as entity registry --- homeassistant/config_entries.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a708df320a1eb..eb35e56dcfda5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -123,6 +123,7 @@ async def async_step_discovery(info): import functools import uuid from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import +import weakref from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant @@ -130,7 +131,6 @@ async def async_step_discovery(info): from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry - _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -223,7 +223,7 @@ class ConfigEntry: __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', 'source', 'connection_class', 'state', '_setup_lock', - '_async_cancel_retry_setup') + 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, options: dict = None, @@ -257,6 +257,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 = [] + # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -387,6 +390,18 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: self.title, component.DOMAIN) return False + def add_update_listener(self, listener): + """Listen for when entry is updated. + + Listener: Callback function(old_entry, new_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 { @@ -512,11 +527,9 @@ def async_update_entry(self, entry, *, data=_UNDEF, options=None): if options: entry.options = options - component = getattr(self.hass.components, entry.domain) - dynamic_options = hasattr(component, 'async_options_updated') - if dynamic_options: - self.hass.async_create_task( - component.async_options_updated(self.hass, entry)) + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() From 2da24e07538ddc74ff9b0418a75edc1a3f1f81a1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 17:14:55 +0100 Subject: [PATCH 08/47] Updated text and usage example --- homeassistant/components/deconz/__init__.py | 2 ++ homeassistant/config_entries.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 69fb988b2c2f0..d2e97980ef539 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -73,6 +73,8 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = gateway + config_entry.add_update_listener(async_options_updated) + if not await gateway.async_setup(): return False diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eb35e56dcfda5..a55485130eb00 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -393,7 +393,7 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: def add_update_listener(self, listener): """Listen for when entry is updated. - Listener: Callback function(old_entry, new_entry) + Listener: Callback function(hass, entry) Returns function to unlisten. """ From ae954e9dd3afb64c2cc54f5852178735814941df Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 19:32:55 +0100 Subject: [PATCH 09/47] Type hints --- homeassistant/config_entries.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a55485130eb00..35571bc8bdf27 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -122,7 +122,7 @@ 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 @@ -390,7 +390,7 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: self.title, component.DOMAIN) return False - def add_update_listener(self, listener): + def add_update_listener(self, listener: Callable) -> Callable: """Listen for when entry is updated. Listener: Callback function(hass, entry) @@ -516,7 +516,13 @@ async def async_load(self) -> None: for entry in config['entries']] @callback +<<<<<<< HEAD def async_update_entry(self, entry, *, data=_UNDEF, options=None): +======= + def async_update_entry( + self, entry: ConfigEntry, *, + data: dict = None, options: dict = None) -> None: +>>>>>>> Type hints """Update a config entry.""" if not data and not options: return @@ -676,7 +682,9 @@ def __init__(self, hass: HomeAssistant) -> None: self.active_options = {} @callback - def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): + def _async_create_flow( + self, handler_key: str, *, + context: dict, data: ConfigEntry) -> data_entry_flow.FlowHandler: """""" flow = HANDLERS[handler_key].async_get_options_flow( data['data'], data['options']) @@ -685,7 +693,8 @@ def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): return flow @callback - def _async_finish_flow(self, flow, result): + def _async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: dict): """""" entry = self.active_options.pop(flow.flow_id) self.hass.config_entries.async_update_entry( From 3b907a5087a765bcb3a7d1e0edbe0669bf7fe5b9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 12 Feb 2019 14:53:39 +0100 Subject: [PATCH 10/47] Add tests for config entry options --- homeassistant/config_entries.py | 6 +-- tests/common.py | 3 +- tests/test_config_entries.py | 80 ++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 35571bc8bdf27..3e8180174afc8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -684,12 +684,12 @@ def __init__(self, hass: HomeAssistant) -> None: @callback def _async_create_flow( self, handler_key: str, *, - context: dict, data: ConfigEntry) -> data_entry_flow.FlowHandler: + context: dict, entry: ConfigEntry) -> data_entry_flow.FlowHandler: """""" flow = HANDLERS[handler_key].async_get_options_flow( - data['data'], data['options']) + entry.data, entry.options) flow.init_step = context['source'] - self.active_options[flow.flow_id] = data + self.active_options[flow.flow_id] = entry return flow @callback diff --git a/tests/common.py b/tests/common.py index 28c6e4c530171..c9f60423d2231 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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=None, 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, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e724680a05b6c..0d5d470e2d965 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest @@ -544,6 +544,44 @@ async def test_updating_entry_data(manager): } +async def test_update_entry_options_and_trigger_listener(hass, manager): + """Test that we can update entry options and trigger listener.""" + entry = MockConfigEntry( + domain='test', + options={'first': True}, + ) + entry.add_to_manager(manager) + + def update_listener(hass, entry): + """Test function.""" + hass.data['update_listener'] = True + + entry.add_update_listener(update_listener) + + manager.async_update_entry(entry, options={ + 'second': True + }) + + assert entry.options == { + 'second': True + } + assert hass.data['update_listener'] is True + + +async def test_update_entry_no_change_does_not_call_save(manager): + """Test that we only call save when new data or options are input.""" + entry = MockConfigEntry( + domain='test', + ) + entry.add_to_manager(manager) + + with patch('homeassistant.config_entries' + '.ConfigEntries._async_schedule_save') as mock_save: + manager.async_update_entry(entry) + + assert not mock_save.mock_calls + + async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain='test') @@ -588,3 +626,43 @@ async def test_setup_retrying_during_unload(hass): assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 + + +async def test_entry_options(hass, manager): + """Test that we can set options on an entry.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + options=None + ) + entry.add_to_manager(manager) + + class TestConfigFlow: + """""" + + @staticmethod + def async_get_options_flow(config, options): + """Config entry static method.""" + class TestOptionsFlowHandler(data_entry_flow.FlowHandler): + """Handle a Test options flow.""" + + def __init__(self, config, options): + """""" + pass + return TestOptionsFlowHandler(config, options) + + config_entries.HANDLERS['test'] = TestConfigFlow() + + flow = manager.options._async_create_flow( + 'test', context={'source': 'test'}, entry=entry) + + result = manager.options._async_finish_flow( + flow, {'data': {'second': True}}) + + assert entry.data == { + 'first': True + } + + assert entry.options == { + 'second': True + } From f9b1446915f4df09a15905958926656937bca42d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:10:31 +0100 Subject: [PATCH 11/47] Nearly there? --- .../components/config/config_entries.py | 40 ++-- homeassistant/config_entries.py | 24 ++- .../components/config/test_config_entries.py | 181 ++++++++++++++++-- tests/test_config_entries.py | 12 +- 4 files changed, 209 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 746b12571b287..26e8be8575112 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -18,7 +18,9 @@ async def async_setup(hass): ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) hass.http.register_view( - OptionsManagerFlowResourceView(hass.config_entries.options.flow)) + OptionManagerFlowIndexView(hass.config_entries.options.flow)) + hass.http.register_view( + OptionManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -47,11 +49,11 @@ 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'] def supports_options(domain): - """""" + """Check if config entry supports options.""" return hasattr( config_entries.HANDLERS[domain], 'async_get_options_flow') @@ -137,6 +139,7 @@ async def get(self, request, flow_id): # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" + print('ConfigManagerFlowResourceView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') @@ -156,26 +159,31 @@ async def get(self, request): return self.json(config_entries.FLOWS) -class OptionsManagerFlowResourceView(FlowManagerResourceView): - """View to interact with the options 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='add') +class OptionManagerFlowIndexView(FlowManagerIndexView): + """View to create config flows.""" - return await super().get(request, flow_id) + 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, flow_id): - """Handle a POST request.""" + async def post(self, request): + """Handle a POST request. + + handler in request is entry_id. + """ + print('ConfigManagerFlowIndexView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') # pylint: disable=no-value-for-parameter - return await super().post(request, flow_id) + return await super().post(request) + + +class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): + """View to interact with the flow manager.""" + + url = '/api/config/config_entries/options/flow/{flow_id}' + name = 'api:config:config_entries:options:flow:resource' diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 3e8180174afc8..147e2b292715d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -681,22 +681,28 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def _async_create_flow( - self, handler_key: str, *, - context: dict, entry: ConfigEntry) -> data_entry_flow.FlowHandler: + async def _async_create_flow( + self, entry_id: str, *, + context: dict, data) -> data_entry_flow.FlowHandler: """""" - flow = HANDLERS[handler_key].async_get_options_flow( + entry = None + for ent in self.hass.config_entries.async_entries(): + if entry_id == ent.entry_id: + entry = ent + break + + flow = HANDLERS[entry.domain].async_get_options_flow( entry.data, entry.options) flow.init_step = context['source'] - self.active_options[flow.flow_id] = entry + self.active_options[entry_id] = entry return flow - @callback - def _async_finish_flow( + async def _async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: dict): """""" - entry = self.active_options.pop(flow.flow_id) + entry = self.active_options.pop(flow.handler) self.hass.config_entries.async_update_entry( entry, options=result['data']) + + result['result'] = True return result diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index be73906c1bfbe..859814fa75ae1 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol -from homeassistant import config_entries as core_ce +from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries @@ -30,25 +30,40 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_entries(hass, client): +async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( - domain='comp', - title='Test 1', - source='bla', - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + domain='comp', + title='Test 1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) MockConfigEntry( - domain='comp2', - title='Test 2', - source='bla2', - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, + domain='comp2', + title='Test 2', + source='bla2', + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) - resp = yield from client.get('/api/config/config_entries/entry') + + class CompConfigFlow: + """""" + @staticmethod + def async_get_options_flow(config, options): + """""" + pass + HANDLERS['comp'] = CompConfigFlow() + + class Comp2ConfigFlow: + """""" + def __init__(self): + """""" + pass + HANDLERS['comp2'] = Comp2ConfigFlow() + + resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() for entry in data: entry.pop('entry_id') assert data == [ @@ -58,6 +73,7 @@ def test_get_entries(hass, client): 'source': 'bla', 'state': 'not_loaded', 'connection_class': 'local_poll', + 'supports_options': True, }, { 'domain': 'comp2', @@ -65,6 +81,7 @@ def test_get_entries(hass, client): 'source': 'bla2', 'state': 'loaded', 'connection_class': 'assumed', + 'supports_options': False, }, ] @@ -467,3 +484,139 @@ async def async_step_user(self, user_input=None): '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 401 + +from homeassistant.core import callback + +async def test_options_flow(hass, client): + """Test we can change options.""" + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_user(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('enabled')] = bool + return self.async_show_form( + step_id='user', + data_schema=schema, + description_placeholders={ + 'enabled': 'Set to true to be true', + } + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + + data.pop('flow_id') + print(data) + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'user', + 'data_schema': [ + { + 'name': 'enabled', + 'required': True, + 'type': 'boolean' + }, + ], + 'description_placeholders': { + 'enabled': 'Set to true to be true', + }, + 'errors': None + } + + + +async def test_two_step_options_flow(hass, client): + """Test we can finish a two step options flow.""" + set_component( + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_user(self, user_input=None): + return self.async_show_form( + step_id='finish', + data_schema=vol.Schema({ + 'enabled': bool + }) + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title='Enable disable', + data=user_input + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + flow_id = data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'finish', + 'data_schema': [ + { + 'name': 'enabled', + 'type': 'boolean' + } + ], + 'description_placeholders': None, + 'errors': None + } + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post( + '/api/config/config_entries/options/flow/{}'.format(flow_id), + json={'enabled': True}) + assert resp.status == 200 + data = await resp.json() + data.pop('flow_id') + assert data == { + 'handler': 'test1', + 'type': 'create_entry', + 'title': 'Enable disable', + 'version': 1, + 'description': None, + 'description_placeholders': None, + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0d5d470e2d965..25f54bc0b44ed 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, patch import pytest @@ -638,25 +638,19 @@ async def test_entry_options(hass, manager): entry.add_to_manager(manager) class TestConfigFlow: - """""" - @staticmethod def async_get_options_flow(config, options): - """Config entry static method.""" class TestOptionsFlowHandler(data_entry_flow.FlowHandler): - """Handle a Test options flow.""" - def __init__(self, config, options): - """""" pass return TestOptionsFlowHandler(config, options) config_entries.HANDLERS['test'] = TestConfigFlow() flow = manager.options._async_create_flow( - 'test', context={'source': 'test'}, entry=entry) + entry.entry_id, context={'source': 'test'}, data=None) - result = manager.options._async_finish_flow( + manager.options._async_finish_flow( flow, {'data': {'second': True}}) assert entry.data == { From f5aa8fd9e2e5636b2f1387a2cf48edcfc8c4a77d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:44:19 +0100 Subject: [PATCH 12/47] Clean up --- .../components/config/config_entries.py | 6 ++-- homeassistant/components/deconz/__init__.py | 5 --- .../components/deconz/config_flow.py | 20 ------------ homeassistant/components/deconz/strings.json | 6 ---- homeassistant/config_entries.py | 31 ++++++++++++------- .../components/config/test_config_entries.py | 4 +-- tests/test_config_entries.py | 18 ++++++----- 7 files changed, 34 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 26e8be8575112..d866fb5f5a7e4 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -159,10 +159,8 @@ async def get(self, request): return self.json(config_entries.FLOWS) - - class OptionManagerFlowIndexView(FlowManagerIndexView): - """View to create config flows.""" + """View to create option flows.""" url = '/api/config/config_entries/entry/option/flow' name = 'api:config:config_entries:entry:resource:option:flow' @@ -183,7 +181,7 @@ async def post(self, request): class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): - """View to interact with the flow manager.""" + """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' diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index d2e97980ef539..79a1a36d57f5e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -163,11 +163,6 @@ async def async_refresh_devices(call): return True -async def async_options_updated(hass, config_entry): - """Options have changed.""" - pass - - async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" gateway = hass.data.pop(DOMAIN) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 741dc7a141e6b..8f90f303fcaad 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -168,23 +168,3 @@ async def async_step_import(self, import_config): user_input = {CONF_ALLOW_CLIP_SENSOR: True, CONF_ALLOW_DECONZ_GROUPS: True} return await self.async_step_options(user_input=user_input) - - @staticmethod - def async_get_options_flow(config, options): - """""" - return DeconzOptionsFlowHandler(config, options) - - -from homeassistant import data_entry_flow - - -class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): - """Handle a deCONZ options flow.""" - - def __init__(self, config, options): - """""" - pass - - def async_step_init(self, user_input=None): - """""" - pass diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 4ec5a6fff70f9..1bf7235713afb 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -29,11 +29,5 @@ "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } - }, - "options": { - "title": "Options for deCONZ Zigbee gateway", - "step": {}, - "error": {}, - "abort": {} } } \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 147e2b292715d..9c8dac7a81661 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -453,6 +453,14 @@ def async_domains(self) -> List[str]: return result + @callback + def async_get_entry(self, entry_id: str) -> 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.""" @@ -672,35 +680,34 @@ def _async_in_progress(self): class Options: - """""" + """Flow to set options for a configuration entry.""" def __init__(self, hass: HomeAssistant) -> None: - """""" + """Initialize the options manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) - self.active_options = {} async def _async_create_flow( self, entry_id: str, *, context: dict, data) -> data_entry_flow.FlowHandler: - """""" - entry = None - for ent in self.hass.config_entries.async_entries(): - if entry_id == ent.entry_id: - entry = ent - break + """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) flow = HANDLERS[entry.domain].async_get_options_flow( entry.data, entry.options) flow.init_step = context['source'] - self.active_options[entry_id] = entry return flow async def _async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: dict): - """""" - entry = self.active_options.pop(flow.handler) + """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) self.hass.config_entries.async_update_entry( entry, options=result['data']) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 859814fa75ae1..719ab2dd5e30f 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -9,6 +9,7 @@ from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS +from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component @@ -485,7 +486,6 @@ async def async_step_user(self, user_input=None): assert resp2.status == 401 -from homeassistant.core import callback async def test_options_flow(hass, client): """Test we can change options.""" @@ -545,7 +545,6 @@ async def async_step_user(self, user_input=None): } - async def test_two_step_options_flow(hass, client): """Test we can finish a two step options flow.""" set_component( @@ -605,6 +604,7 @@ async def async_step_finish(self, user_input=None): 'errors': None } + with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post( '/api/config/config_entries/options/flow/{}'.format(flow_id), diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 25f54bc0b44ed..2ccb67104a0c2 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -637,20 +638,23 @@ async def test_entry_options(hass, manager): ) entry.add_to_manager(manager) - class TestConfigFlow: + class TestFlow: @staticmethod + @callback def async_get_options_flow(config, options): - class TestOptionsFlowHandler(data_entry_flow.FlowHandler): + class OptionsFlowHandler(data_entry_flow.FlowHandler): def __init__(self, config, options): pass - return TestOptionsFlowHandler(config, options) + return OptionsFlowHandler(config, options) - config_entries.HANDLERS['test'] = TestConfigFlow() - - flow = manager.options._async_create_flow( + config_entries.HANDLERS['test'] = TestFlow() + print(entry) + flow = await manager.options._async_create_flow( entry.entry_id, context={'source': 'test'}, data=None) - manager.options._async_finish_flow( + flow.handler = entry.entry_id # Used to keep reference to config entry + + await manager.options._async_finish_flow( flow, {'data': {'second': True}}) assert entry.data == { From 2f57642b79bbefb9e83c6bae4c39c863157dcbc1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:46:18 +0100 Subject: [PATCH 13/47] Remove stale prints --- homeassistant/components/config/config_entries.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d866fb5f5a7e4..6bafcc24f5fb5 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -139,7 +139,6 @@ async def get(self, request, flow_id): # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" - print('ConfigManagerFlowResourceView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') @@ -171,7 +170,6 @@ async def post(self, request): handler in request is entry_id. """ - print('ConfigManagerFlowIndexView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') From bf06d3e40a141fbffd2356e1b1a6453eebcfcb30 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:49:32 +0100 Subject: [PATCH 14/47] Fix hound comments --- homeassistant/components/deconz/__init__.py | 2 -- tests/components/config/test_config_entries.py | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 79a1a36d57f5e..8015324be13fd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -73,8 +73,6 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = gateway - config_entry.add_update_listener(async_options_updated) - if not await gateway.async_setup(): return False diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 719ab2dd5e30f..44600accbec26 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -604,7 +604,6 @@ async def async_step_finish(self, user_input=None): 'errors': None } - with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post( '/api/config/config_entries/options/flow/{}'.format(flow_id), From 27d46793a2c0d3c79a7ac989b0bb60daa9ad58d5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 17:01:34 +0100 Subject: [PATCH 15/47] Fix empty docstring in test --- tests/components/config/test_config_entries.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 44600accbec26..4dd1f3f46bca4 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -48,17 +48,14 @@ async def test_get_entries(hass, client): ).add_to_hass(hass) class CompConfigFlow: - """""" @staticmethod + @callback def async_get_options_flow(config, options): - """""" pass HANDLERS['comp'] = CompConfigFlow() class Comp2ConfigFlow: - """""" def __init__(self): - """""" pass HANDLERS['comp2'] = Comp2ConfigFlow() From 1b1cec1acc2d09de9333f485923b1891b15a491b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 18:08:04 +0100 Subject: [PATCH 16/47] Fix typing --- homeassistant/config_entries.py | 23 ++++++++++++----------- tests/test_config_entries.py | 3 +-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9c8dac7a81661..1d171812d3b35 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,7 @@ class ConfigEntry: 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, options: dict = None, + 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.""" @@ -258,7 +258,7 @@ def __init__(self, version: str, domain: str, title: str, data: dict, self.state = state # Listeners to call on update - self.update_listeners = [] + self.update_listeners: list = [] # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -454,7 +454,7 @@ def async_domains(self) -> List[str]: return result @callback - def async_get_entry(self, entry_id: str) -> ConfigEntry: + 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: @@ -524,15 +524,12 @@ async def async_load(self) -> None: for entry in config['entries']] @callback -<<<<<<< HEAD - def async_update_entry(self, entry, *, data=_UNDEF, options=None): -======= def async_update_entry( self, entry: ConfigEntry, *, - data: dict = None, options: dict = None) -> None: ->>>>>>> Type hints + data: Optional[dict] = _UNDEF, + options: Optional[dict] = None) -> None: """Update a config entry.""" - if not data and not options: + if data is _UNDEF and not options: return if data is not _UNDEF: @@ -594,7 +591,7 @@ async def _async_finish_flow(self, flow, result): domain=result['handler'], title=result['title'], data=result['data'], - options=None, + options={}, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -644,7 +641,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) @@ -696,6 +693,8 @@ async def _async_create_flow( 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) flow.init_step = context['source'] @@ -708,6 +707,8 @@ async def _async_finish_flow( 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']) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2ccb67104a0c2..a6b97855c10d3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -579,8 +579,7 @@ async def test_update_entry_no_change_does_not_call_save(manager): with patch('homeassistant.config_entries' '.ConfigEntries._async_schedule_save') as mock_save: manager.async_update_entry(entry) - - assert not mock_save.mock_calls + assert not mock_save.mock_calls async def test_setup_raise_not_ready(hass, caplog): From dc82dc13a7cf9d733d083ff50a4bcb82f029276f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 18:12:42 +0100 Subject: [PATCH 17/47] Fix line too long --- homeassistant/config_entries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1d171812d3b35..299dcfc6826e4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,8 @@ class ConfigEntry: '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, + 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.""" From c0f4e432c77ea7fae266060c8a28e1cf6ee6e455 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 2 Dec 2018 18:07:06 +0100 Subject: [PATCH 18/47] RFC commit --- .../components/deconz/config_flow.py | 22 +++++ homeassistant/config_entries.py | 92 ++++++++++++++++++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 8f90f303fcaad..0fa72ea6201ba 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -168,3 +168,25 @@ async def async_step_import(self, import_config): user_input = {CONF_ALLOW_CLIP_SENSOR: True, CONF_ALLOW_DECONZ_GROUPS: True} return await self.async_step_options(user_input=user_input) + + @staticmethod + def async_get_options_flow(self): + """""" + return DeconzOptionsFlowHandler + + +from homeassistant import data_entry_flow + + +class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): + """Handle a deCONZ options flow.""" + + VERSION = 1 + + def __init__(self): + """""" + pass + + def async_step_user(self, user_input=None): + """""" + pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index cb79f457ce57b..df7e36971fb77 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -221,12 +221,12 @@ 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', + __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', + 'source', 'connection_class', 'state', '_setup_lock', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, + source: str, connection_class: str, options: dict = {}, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -245,6 +245,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict, # Config data self.data = data + # Entry options + self.options = options + # Source of the configuration (user, discovery, cloud) self.source = source @@ -392,6 +395,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, } @@ -416,6 +420,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 = Options(hass) self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -475,6 +480,7 @@ async def async_load(self) -> None: self.hass.config.path(PATH_CONFIG), self._store, old_conf_migrate_func=_old_conf_migrator ) + print('async load config entries', config) if config is None: self._entries = [] @@ -490,14 +496,29 @@ 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.8x + 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=None): """Update a config entry.""" + if not data and not options: + return + if data is not _UNDEF: entry.data = data + + if options: + entry.options = options + + component = getattr(self.hass.components, entry.domain) + dynamic_options = hasattr(component, 'async_options_updated') + if dynamic_options: + self.hass.async_create_task( + component.async_options_updated(self.hass, entry)) + self._async_schedule_save() async def async_forward_entry_setup(self, entry, component): @@ -629,3 +650,64 @@ 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] + + @staticmethod + def async_get_options_flow(): + """""" + return None + + +class Options: + """""" + + def __init__(self, hass: HomeAssistant) -> None: + """""" + self.hass = hass + self.flow = data_entry_flow.FlowManager( + hass, self._async_create_flow, self._async_finish_flow) + self.active_options = {} + + @callback + def async_domains(self) -> List[str]: + """Return domains for which we have options.""" + result = [] + + for domain in FLOWS: + if HANDLERS[domain].async_get_options_flow(): + result.append(domain) + + return result + + @callback + def async_active_flow(self, entry: ConfigEntry): + """""" + if entry in self.active_options.values(): + return True + return False + + + @callback + def _async_create_flow(self, handler, context, data: ConfigEntry): + """""" + handler = HANDLERS[handler].async_get_options_flow() + flow = handler(data['data']) + flow.init_step = context['source'] + self.active_options[flow.flow_id] = data + return flow + + @callback + def _async_finish_flow(self, flow, result): + """""" + entry = self.active_options.pop(flow.flow_id) + self.hass.config_entries.async_update_entry( + entry, options=result['data']) + return result + + +class OptionsFlow(data_entry_flow.FlowHandler): + """""" + + @property + def data(self): + """""" + return self._config_entry['data'] From b91b5c8185dd5de513931a0e0b25088c575bb95d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 12 Jan 2019 10:15:50 +0100 Subject: [PATCH 19/47] Stash changes --- .../components/deconz/config_flow.py | 2 +- homeassistant/config_entries.py | 23 +------------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 0fa72ea6201ba..d3603aed458cb 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -187,6 +187,6 @@ def __init__(self): """""" pass - def async_step_user(self, user_input=None): + def async_step_init(self, user_input=None): """""" pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index df7e36971fb77..5d8d5eb0d7fd0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,7 @@ class ConfigEntry: '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, options: dict = {}, + source: str, connection_class: str, options: dict = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -667,17 +667,6 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def async_domains(self) -> List[str]: - """Return domains for which we have options.""" - result = [] - - for domain in FLOWS: - if HANDLERS[domain].async_get_options_flow(): - result.append(domain) - - return result - @callback def async_active_flow(self, entry: ConfigEntry): """""" @@ -685,7 +674,6 @@ def async_active_flow(self, entry: ConfigEntry): return True return False - @callback def _async_create_flow(self, handler, context, data: ConfigEntry): """""" @@ -702,12 +690,3 @@ def _async_finish_flow(self, flow, result): self.hass.config_entries.async_update_entry( entry, options=result['data']) return result - - -class OptionsFlow(data_entry_flow.FlowHandler): - """""" - - @property - def data(self): - """""" - return self._config_entry['data'] From c5403d62edc2b423cdf81aea62b202cbc57aa839 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Jan 2019 21:13:10 +0100 Subject: [PATCH 20/47] Puzzle pieces --- homeassistant/components/deconz/__init__.py | 5 +++++ homeassistant/components/deconz/config_flow.py | 2 +- homeassistant/components/deconz/strings.json | 6 ++++++ homeassistant/config_entries.py | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8015324be13fd..69fb988b2c2f0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -161,6 +161,11 @@ async def async_refresh_devices(call): return True +async def async_options_updated(hass, config_entry): + """Options have changed.""" + pass + + async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" gateway = hass.data.pop(DOMAIN) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d3603aed458cb..68a26c6a17934 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -183,7 +183,7 @@ class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): VERSION = 1 - def __init__(self): + def __init__(self, config, options): """""" pass diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 1bf7235713afb..4ec5a6fff70f9 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -29,5 +29,11 @@ "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } + }, + "options": { + "title": "Options for deCONZ Zigbee gateway", + "step": {}, + "error": {}, + "abort": {} } } \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5d8d5eb0d7fd0..013a415c8a017 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -568,6 +568,7 @@ async def _async_finish_flow(self, flow, result): domain=result['handler'], title=result['title'], data=result['data'], + options=None, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -678,7 +679,7 @@ def async_active_flow(self, entry: ConfigEntry): def _async_create_flow(self, handler, context, data: ConfigEntry): """""" handler = HANDLERS[handler].async_get_options_flow() - flow = handler(data['data']) + flow = handler(data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data return flow From d0a51fbca0e16db6d335bd520404117c21562d2b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 2 Feb 2019 12:17:37 +0100 Subject: [PATCH 21/47] Getting closer --- .../components/config/config_entries.py | 36 +++++++++++++++++++ .../components/deconz/config_flow.py | 2 +- homeassistant/config_entries.py | 4 +-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 68890a79ca653..f1ec8d8fabc6a 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -17,6 +17,8 @@ async def async_setup(hass): hass.http.register_view( ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) + hass.http.register_view( + OptionsManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -47,6 +49,14 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request): """List flows in progress.""" hass = request.app['hass'] + + def supports_options(domain): + """""" + if domain == 'axis': + return None + handler = config_entries.HANDLERS[domain] + return handler.async_get_options_flow() is not None + return self.json([{ 'entry_id': entry.entry_id, 'domain': entry.domain, @@ -54,6 +64,7 @@ async def get(self, request): 'source': entry.source, 'state': entry.state, 'connection_class': entry.connection_class, + 'supports_options': supports_options(entry.domain), } for entry in hass.config_entries.async_entries()]) @@ -145,3 +156,28 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) + + +class OptionsManagerFlowResourceView(FlowManagerResourceView): + """View to interact with the options 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='add') + + 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='add') + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 68a26c6a17934..27d31580eb366 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -170,7 +170,7 @@ async def async_step_import(self, import_config): return await self.async_step_options(user_input=user_input) @staticmethod - def async_get_options_flow(self): + def async_get_options_flow(): """""" return DeconzOptionsFlowHandler diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 013a415c8a017..90d00887f364c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -676,9 +676,9 @@ def async_active_flow(self, entry: ConfigEntry): return False @callback - def _async_create_flow(self, handler, context, data: ConfigEntry): + def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): """""" - handler = HANDLERS[handler].async_get_options_flow() + handler = HANDLERS[handler_key].async_get_options_flow() flow = handler(data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data From c2a42a959fb208b35750a05b1b568dad57a9c688 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Feb 2019 17:31:18 +0100 Subject: [PATCH 22/47] Remove debug stuff --- homeassistant/components/config/config_entries.py | 2 -- homeassistant/components/deconz/config_flow.py | 2 -- homeassistant/config_entries.py | 1 - 3 files changed, 5 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index f1ec8d8fabc6a..7d1c034b3fc13 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -52,8 +52,6 @@ async def get(self, request): def supports_options(domain): """""" - if domain == 'axis': - return None handler = config_entries.HANDLERS[domain] return handler.async_get_options_flow() is not None diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 27d31580eb366..3b40984293b13 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -181,8 +181,6 @@ def async_get_options_flow(): class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): """Handle a deCONZ options flow.""" - VERSION = 1 - def __init__(self, config, options): """""" pass diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 90d00887f364c..2c33a83d2dd31 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -480,7 +480,6 @@ async def async_load(self) -> None: self.hass.config.path(PATH_CONFIG), self._store, old_conf_migrate_func=_old_conf_migrator ) - print('async load config entries', config) if config is None: self._entries = [] From 97a8b9e42775ea222b1770bdb1d249475ac79081 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Feb 2019 00:05:59 +0100 Subject: [PATCH 23/47] Fix up following suggestions from Balloob --- .../components/config/config_entries.py | 4 ++-- homeassistant/components/deconz/config_flow.py | 4 ++-- homeassistant/config_entries.py | 16 ++-------------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 7d1c034b3fc13..746b12571b287 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -52,8 +52,8 @@ async def get(self, request): def supports_options(domain): """""" - handler = config_entries.HANDLERS[domain] - return handler.async_get_options_flow() is not None + return hasattr( + config_entries.HANDLERS[domain], 'async_get_options_flow') return self.json([{ 'entry_id': entry.entry_id, diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 3b40984293b13..741dc7a141e6b 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -170,9 +170,9 @@ async def async_step_import(self, import_config): return await self.async_step_options(user_input=user_input) @staticmethod - def async_get_options_flow(): + def async_get_options_flow(config, options): """""" - return DeconzOptionsFlowHandler + return DeconzOptionsFlowHandler(config, options) from homeassistant import data_entry_flow diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2c33a83d2dd31..a708df320a1eb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -651,11 +651,6 @@ def _async_in_progress(self): if flw['handler'] == self.handler and flw['flow_id'] != self.flow_id] - @staticmethod - def async_get_options_flow(): - """""" - return None - class Options: """""" @@ -667,18 +662,11 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def async_active_flow(self, entry: ConfigEntry): - """""" - if entry in self.active_options.values(): - return True - return False - @callback def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): """""" - handler = HANDLERS[handler_key].async_get_options_flow() - flow = handler(data['data'], data['options']) + flow = HANDLERS[handler_key].async_get_options_flow( + data['data'], data['options']) flow.init_step = context['source'] self.active_options[flow.flow_id] = data return flow From aad7a6ae7900e6e6d5afc24cd0de26450976a233 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 17:08:49 +0100 Subject: [PATCH 24/47] Use similar implementation to update callbacks as entity registry --- homeassistant/config_entries.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a708df320a1eb..eb35e56dcfda5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -123,6 +123,7 @@ async def async_step_discovery(info): import functools import uuid from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import +import weakref from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant @@ -130,7 +131,6 @@ async def async_step_discovery(info): from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry - _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -223,7 +223,7 @@ class ConfigEntry: __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', 'source', 'connection_class', 'state', '_setup_lock', - '_async_cancel_retry_setup') + 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, options: dict = None, @@ -257,6 +257,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 = [] + # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -387,6 +390,18 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: self.title, component.DOMAIN) return False + def add_update_listener(self, listener): + """Listen for when entry is updated. + + Listener: Callback function(old_entry, new_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 { @@ -512,11 +527,9 @@ def async_update_entry(self, entry, *, data=_UNDEF, options=None): if options: entry.options = options - component = getattr(self.hass.components, entry.domain) - dynamic_options = hasattr(component, 'async_options_updated') - if dynamic_options: - self.hass.async_create_task( - component.async_options_updated(self.hass, entry)) + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() From e8aa1b27d045ca0335818248aaec4c45b9bba082 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 17:14:55 +0100 Subject: [PATCH 25/47] Updated text and usage example --- homeassistant/components/deconz/__init__.py | 2 ++ homeassistant/config_entries.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 69fb988b2c2f0..d2e97980ef539 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -73,6 +73,8 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = gateway + config_entry.add_update_listener(async_options_updated) + if not await gateway.async_setup(): return False diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eb35e56dcfda5..a55485130eb00 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -393,7 +393,7 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: def add_update_listener(self, listener): """Listen for when entry is updated. - Listener: Callback function(old_entry, new_entry) + Listener: Callback function(hass, entry) Returns function to unlisten. """ From 296f5aeb41ffa97a01cc90ec0f469dba3ae02b38 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 10 Feb 2019 19:32:55 +0100 Subject: [PATCH 26/47] Type hints --- homeassistant/config_entries.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a55485130eb00..35571bc8bdf27 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -122,7 +122,7 @@ 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 @@ -390,7 +390,7 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: self.title, component.DOMAIN) return False - def add_update_listener(self, listener): + def add_update_listener(self, listener: Callable) -> Callable: """Listen for when entry is updated. Listener: Callback function(hass, entry) @@ -516,7 +516,13 @@ async def async_load(self) -> None: for entry in config['entries']] @callback +<<<<<<< HEAD def async_update_entry(self, entry, *, data=_UNDEF, options=None): +======= + def async_update_entry( + self, entry: ConfigEntry, *, + data: dict = None, options: dict = None) -> None: +>>>>>>> Type hints """Update a config entry.""" if not data and not options: return @@ -676,7 +682,9 @@ def __init__(self, hass: HomeAssistant) -> None: self.active_options = {} @callback - def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): + def _async_create_flow( + self, handler_key: str, *, + context: dict, data: ConfigEntry) -> data_entry_flow.FlowHandler: """""" flow = HANDLERS[handler_key].async_get_options_flow( data['data'], data['options']) @@ -685,7 +693,8 @@ def _async_create_flow(self, handler_key, *, context, data: ConfigEntry): return flow @callback - def _async_finish_flow(self, flow, result): + def _async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: dict): """""" entry = self.active_options.pop(flow.flow_id) self.hass.config_entries.async_update_entry( From ccf95e3c562cff6ea141953e054af2a4ce0bdee7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 12 Feb 2019 14:53:39 +0100 Subject: [PATCH 27/47] Add tests for config entry options --- homeassistant/config_entries.py | 6 +-- tests/common.py | 3 +- tests/test_config_entries.py | 80 ++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 35571bc8bdf27..3e8180174afc8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -684,12 +684,12 @@ def __init__(self, hass: HomeAssistant) -> None: @callback def _async_create_flow( self, handler_key: str, *, - context: dict, data: ConfigEntry) -> data_entry_flow.FlowHandler: + context: dict, entry: ConfigEntry) -> data_entry_flow.FlowHandler: """""" flow = HANDLERS[handler_key].async_get_options_flow( - data['data'], data['options']) + entry.data, entry.options) flow.init_step = context['source'] - self.active_options[flow.flow_id] = data + self.active_options[flow.flow_id] = entry return flow @callback diff --git a/tests/common.py b/tests/common.py index 28c6e4c530171..c9f60423d2231 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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=None, 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, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e724680a05b6c..0d5d470e2d965 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest @@ -544,6 +544,44 @@ async def test_updating_entry_data(manager): } +async def test_update_entry_options_and_trigger_listener(hass, manager): + """Test that we can update entry options and trigger listener.""" + entry = MockConfigEntry( + domain='test', + options={'first': True}, + ) + entry.add_to_manager(manager) + + def update_listener(hass, entry): + """Test function.""" + hass.data['update_listener'] = True + + entry.add_update_listener(update_listener) + + manager.async_update_entry(entry, options={ + 'second': True + }) + + assert entry.options == { + 'second': True + } + assert hass.data['update_listener'] is True + + +async def test_update_entry_no_change_does_not_call_save(manager): + """Test that we only call save when new data or options are input.""" + entry = MockConfigEntry( + domain='test', + ) + entry.add_to_manager(manager) + + with patch('homeassistant.config_entries' + '.ConfigEntries._async_schedule_save') as mock_save: + manager.async_update_entry(entry) + + assert not mock_save.mock_calls + + async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain='test') @@ -588,3 +626,43 @@ async def test_setup_retrying_during_unload(hass): assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 + + +async def test_entry_options(hass, manager): + """Test that we can set options on an entry.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + options=None + ) + entry.add_to_manager(manager) + + class TestConfigFlow: + """""" + + @staticmethod + def async_get_options_flow(config, options): + """Config entry static method.""" + class TestOptionsFlowHandler(data_entry_flow.FlowHandler): + """Handle a Test options flow.""" + + def __init__(self, config, options): + """""" + pass + return TestOptionsFlowHandler(config, options) + + config_entries.HANDLERS['test'] = TestConfigFlow() + + flow = manager.options._async_create_flow( + 'test', context={'source': 'test'}, entry=entry) + + result = manager.options._async_finish_flow( + flow, {'data': {'second': True}}) + + assert entry.data == { + 'first': True + } + + assert entry.options == { + 'second': True + } From 191a6033375eab8f095442da885f276ee984d6fb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:10:31 +0100 Subject: [PATCH 28/47] Nearly there? --- .../components/config/config_entries.py | 40 ++-- homeassistant/config_entries.py | 24 ++- .../components/config/test_config_entries.py | 181 ++++++++++++++++-- tests/test_config_entries.py | 12 +- 4 files changed, 209 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 746b12571b287..26e8be8575112 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -18,7 +18,9 @@ async def async_setup(hass): ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) hass.http.register_view( - OptionsManagerFlowResourceView(hass.config_entries.options.flow)) + OptionManagerFlowIndexView(hass.config_entries.options.flow)) + hass.http.register_view( + OptionManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -47,11 +49,11 @@ 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'] def supports_options(domain): - """""" + """Check if config entry supports options.""" return hasattr( config_entries.HANDLERS[domain], 'async_get_options_flow') @@ -137,6 +139,7 @@ async def get(self, request, flow_id): # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" + print('ConfigManagerFlowResourceView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') @@ -156,26 +159,31 @@ async def get(self, request): return self.json(config_entries.FLOWS) -class OptionsManagerFlowResourceView(FlowManagerResourceView): - """View to interact with the options 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='add') +class OptionManagerFlowIndexView(FlowManagerIndexView): + """View to create config flows.""" - return await super().get(request, flow_id) + 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, flow_id): - """Handle a POST request.""" + async def post(self, request): + """Handle a POST request. + + handler in request is entry_id. + """ + print('ConfigManagerFlowIndexView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') # pylint: disable=no-value-for-parameter - return await super().post(request, flow_id) + return await super().post(request) + + +class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): + """View to interact with the flow manager.""" + + url = '/api/config/config_entries/options/flow/{flow_id}' + name = 'api:config:config_entries:options:flow:resource' diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 3e8180174afc8..147e2b292715d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -681,22 +681,28 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow) self.active_options = {} - @callback - def _async_create_flow( - self, handler_key: str, *, - context: dict, entry: ConfigEntry) -> data_entry_flow.FlowHandler: + async def _async_create_flow( + self, entry_id: str, *, + context: dict, data) -> data_entry_flow.FlowHandler: """""" - flow = HANDLERS[handler_key].async_get_options_flow( + entry = None + for ent in self.hass.config_entries.async_entries(): + if entry_id == ent.entry_id: + entry = ent + break + + flow = HANDLERS[entry.domain].async_get_options_flow( entry.data, entry.options) flow.init_step = context['source'] - self.active_options[flow.flow_id] = entry + self.active_options[entry_id] = entry return flow - @callback - def _async_finish_flow( + async def _async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: dict): """""" - entry = self.active_options.pop(flow.flow_id) + entry = self.active_options.pop(flow.handler) self.hass.config_entries.async_update_entry( entry, options=result['data']) + + result['result'] = True return result diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index be73906c1bfbe..859814fa75ae1 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol -from homeassistant import config_entries as core_ce +from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries @@ -30,25 +30,40 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_entries(hass, client): +async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( - domain='comp', - title='Test 1', - source='bla', - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + domain='comp', + title='Test 1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) MockConfigEntry( - domain='comp2', - title='Test 2', - source='bla2', - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, + domain='comp2', + title='Test 2', + source='bla2', + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) - resp = yield from client.get('/api/config/config_entries/entry') + + class CompConfigFlow: + """""" + @staticmethod + def async_get_options_flow(config, options): + """""" + pass + HANDLERS['comp'] = CompConfigFlow() + + class Comp2ConfigFlow: + """""" + def __init__(self): + """""" + pass + HANDLERS['comp2'] = Comp2ConfigFlow() + + resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() for entry in data: entry.pop('entry_id') assert data == [ @@ -58,6 +73,7 @@ def test_get_entries(hass, client): 'source': 'bla', 'state': 'not_loaded', 'connection_class': 'local_poll', + 'supports_options': True, }, { 'domain': 'comp2', @@ -65,6 +81,7 @@ def test_get_entries(hass, client): 'source': 'bla2', 'state': 'loaded', 'connection_class': 'assumed', + 'supports_options': False, }, ] @@ -467,3 +484,139 @@ async def async_step_user(self, user_input=None): '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 401 + +from homeassistant.core import callback + +async def test_options_flow(hass, client): + """Test we can change options.""" + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_user(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('enabled')] = bool + return self.async_show_form( + step_id='user', + data_schema=schema, + description_placeholders={ + 'enabled': 'Set to true to be true', + } + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + + data.pop('flow_id') + print(data) + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'user', + 'data_schema': [ + { + 'name': 'enabled', + 'required': True, + 'type': 'boolean' + }, + ], + 'description_placeholders': { + 'enabled': 'Set to true to be true', + }, + 'errors': None + } + + + +async def test_two_step_options_flow(hass, client): + """Test we can finish a two step options flow.""" + set_component( + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_user(self, user_input=None): + return self.async_show_form( + step_id='finish', + data_schema=vol.Schema({ + 'enabled': bool + }) + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title='Enable disable', + data=user_input + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + flow_id = data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'finish', + 'data_schema': [ + { + 'name': 'enabled', + 'type': 'boolean' + } + ], + 'description_placeholders': None, + 'errors': None + } + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post( + '/api/config/config_entries/options/flow/{}'.format(flow_id), + json={'enabled': True}) + assert resp.status == 200 + data = await resp.json() + data.pop('flow_id') + assert data == { + 'handler': 'test1', + 'type': 'create_entry', + 'title': 'Enable disable', + 'version': 1, + 'description': None, + 'description_placeholders': None, + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0d5d470e2d965..25f54bc0b44ed 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, patch import pytest @@ -638,25 +638,19 @@ async def test_entry_options(hass, manager): entry.add_to_manager(manager) class TestConfigFlow: - """""" - @staticmethod def async_get_options_flow(config, options): - """Config entry static method.""" class TestOptionsFlowHandler(data_entry_flow.FlowHandler): - """Handle a Test options flow.""" - def __init__(self, config, options): - """""" pass return TestOptionsFlowHandler(config, options) config_entries.HANDLERS['test'] = TestConfigFlow() flow = manager.options._async_create_flow( - 'test', context={'source': 'test'}, entry=entry) + entry.entry_id, context={'source': 'test'}, data=None) - result = manager.options._async_finish_flow( + manager.options._async_finish_flow( flow, {'data': {'second': True}}) assert entry.data == { From 51c8acf7cce9fb6f28e211e41d51aa29090d350b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:44:19 +0100 Subject: [PATCH 29/47] Clean up --- .../components/config/config_entries.py | 6 ++-- homeassistant/components/deconz/__init__.py | 5 --- .../components/deconz/config_flow.py | 20 ------------ homeassistant/components/deconz/strings.json | 6 ---- homeassistant/config_entries.py | 31 ++++++++++++------- .../components/config/test_config_entries.py | 4 +-- tests/test_config_entries.py | 18 ++++++----- 7 files changed, 34 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 26e8be8575112..d866fb5f5a7e4 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -159,10 +159,8 @@ async def get(self, request): return self.json(config_entries.FLOWS) - - class OptionManagerFlowIndexView(FlowManagerIndexView): - """View to create config flows.""" + """View to create option flows.""" url = '/api/config/config_entries/entry/option/flow' name = 'api:config:config_entries:entry:resource:option:flow' @@ -183,7 +181,7 @@ async def post(self, request): class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): - """View to interact with the flow manager.""" + """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' diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index d2e97980ef539..79a1a36d57f5e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -163,11 +163,6 @@ async def async_refresh_devices(call): return True -async def async_options_updated(hass, config_entry): - """Options have changed.""" - pass - - async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" gateway = hass.data.pop(DOMAIN) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 741dc7a141e6b..8f90f303fcaad 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -168,23 +168,3 @@ async def async_step_import(self, import_config): user_input = {CONF_ALLOW_CLIP_SENSOR: True, CONF_ALLOW_DECONZ_GROUPS: True} return await self.async_step_options(user_input=user_input) - - @staticmethod - def async_get_options_flow(config, options): - """""" - return DeconzOptionsFlowHandler(config, options) - - -from homeassistant import data_entry_flow - - -class DeconzOptionsFlowHandler(data_entry_flow.FlowHandler): - """Handle a deCONZ options flow.""" - - def __init__(self, config, options): - """""" - pass - - def async_step_init(self, user_input=None): - """""" - pass diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 4ec5a6fff70f9..1bf7235713afb 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -29,11 +29,5 @@ "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } - }, - "options": { - "title": "Options for deCONZ Zigbee gateway", - "step": {}, - "error": {}, - "abort": {} } } \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 147e2b292715d..9c8dac7a81661 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -453,6 +453,14 @@ def async_domains(self) -> List[str]: return result + @callback + def async_get_entry(self, entry_id: str) -> 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.""" @@ -672,35 +680,34 @@ def _async_in_progress(self): class Options: - """""" + """Flow to set options for a configuration entry.""" def __init__(self, hass: HomeAssistant) -> None: - """""" + """Initialize the options manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) - self.active_options = {} async def _async_create_flow( self, entry_id: str, *, context: dict, data) -> data_entry_flow.FlowHandler: - """""" - entry = None - for ent in self.hass.config_entries.async_entries(): - if entry_id == ent.entry_id: - entry = ent - break + """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) flow = HANDLERS[entry.domain].async_get_options_flow( entry.data, entry.options) flow.init_step = context['source'] - self.active_options[entry_id] = entry return flow async def _async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: dict): - """""" - entry = self.active_options.pop(flow.handler) + """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) self.hass.config_entries.async_update_entry( entry, options=result['data']) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 859814fa75ae1..719ab2dd5e30f 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -9,6 +9,7 @@ from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS +from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component @@ -485,7 +486,6 @@ async def async_step_user(self, user_input=None): assert resp2.status == 401 -from homeassistant.core import callback async def test_options_flow(hass, client): """Test we can change options.""" @@ -545,7 +545,6 @@ async def async_step_user(self, user_input=None): } - async def test_two_step_options_flow(hass, client): """Test we can finish a two step options flow.""" set_component( @@ -605,6 +604,7 @@ async def async_step_finish(self, user_input=None): 'errors': None } + with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post( '/api/config/config_entries/options/flow/{}'.format(flow_id), diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 25f54bc0b44ed..2ccb67104a0c2 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -637,20 +638,23 @@ async def test_entry_options(hass, manager): ) entry.add_to_manager(manager) - class TestConfigFlow: + class TestFlow: @staticmethod + @callback def async_get_options_flow(config, options): - class TestOptionsFlowHandler(data_entry_flow.FlowHandler): + class OptionsFlowHandler(data_entry_flow.FlowHandler): def __init__(self, config, options): pass - return TestOptionsFlowHandler(config, options) + return OptionsFlowHandler(config, options) - config_entries.HANDLERS['test'] = TestConfigFlow() - - flow = manager.options._async_create_flow( + config_entries.HANDLERS['test'] = TestFlow() + print(entry) + flow = await manager.options._async_create_flow( entry.entry_id, context={'source': 'test'}, data=None) - manager.options._async_finish_flow( + flow.handler = entry.entry_id # Used to keep reference to config entry + + await manager.options._async_finish_flow( flow, {'data': {'second': True}}) assert entry.data == { From a95346be1fa6c5d44739ea185efb4854970f8d52 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:46:18 +0100 Subject: [PATCH 30/47] Remove stale prints --- homeassistant/components/config/config_entries.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d866fb5f5a7e4..6bafcc24f5fb5 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -139,7 +139,6 @@ async def get(self, request, flow_id): # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" - print('ConfigManagerFlowResourceView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') @@ -171,7 +170,6 @@ async def post(self, request): handler in request is entry_id. """ - print('ConfigManagerFlowIndexView post', request) if not request['hass_user'].is_admin: raise Unauthorized( perm_category=CAT_CONFIG_ENTRIES, permission='add') From 9561eec5d0780bde9057a30eb5e8c1a2cfa91d5d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Feb 2019 21:49:32 +0100 Subject: [PATCH 31/47] Fix hound comments --- homeassistant/components/deconz/__init__.py | 2 -- tests/components/config/test_config_entries.py | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 79a1a36d57f5e..8015324be13fd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -73,8 +73,6 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = gateway - config_entry.add_update_listener(async_options_updated) - if not await gateway.async_setup(): return False diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 719ab2dd5e30f..44600accbec26 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -604,7 +604,6 @@ async def async_step_finish(self, user_input=None): 'errors': None } - with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post( '/api/config/config_entries/options/flow/{}'.format(flow_id), From 4eff8804000e7b10afa47f1aebc2a87efddc2292 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 17:01:34 +0100 Subject: [PATCH 32/47] Fix empty docstring in test --- tests/components/config/test_config_entries.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 44600accbec26..4dd1f3f46bca4 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -48,17 +48,14 @@ async def test_get_entries(hass, client): ).add_to_hass(hass) class CompConfigFlow: - """""" @staticmethod + @callback def async_get_options_flow(config, options): - """""" pass HANDLERS['comp'] = CompConfigFlow() class Comp2ConfigFlow: - """""" def __init__(self): - """""" pass HANDLERS['comp2'] = Comp2ConfigFlow() From 8c925a2a6784ee420336a2188eb853e33fef95aa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 18:08:04 +0100 Subject: [PATCH 33/47] Fix typing --- homeassistant/config_entries.py | 23 ++++++++++++----------- tests/test_config_entries.py | 3 +-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9c8dac7a81661..1d171812d3b35 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,7 @@ class ConfigEntry: 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, options: dict = None, + 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.""" @@ -258,7 +258,7 @@ def __init__(self, version: str, domain: str, title: str, data: dict, self.state = state # Listeners to call on update - self.update_listeners = [] + self.update_listeners: list = [] # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -454,7 +454,7 @@ def async_domains(self) -> List[str]: return result @callback - def async_get_entry(self, entry_id: str) -> ConfigEntry: + 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: @@ -524,15 +524,12 @@ async def async_load(self) -> None: for entry in config['entries']] @callback -<<<<<<< HEAD - def async_update_entry(self, entry, *, data=_UNDEF, options=None): -======= def async_update_entry( self, entry: ConfigEntry, *, - data: dict = None, options: dict = None) -> None: ->>>>>>> Type hints + data: Optional[dict] = _UNDEF, + options: Optional[dict] = None) -> None: """Update a config entry.""" - if not data and not options: + if data is _UNDEF and not options: return if data is not _UNDEF: @@ -594,7 +591,7 @@ async def _async_finish_flow(self, flow, result): domain=result['handler'], title=result['title'], data=result['data'], - options=None, + options={}, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -644,7 +641,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) @@ -696,6 +693,8 @@ async def _async_create_flow( 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) flow.init_step = context['source'] @@ -708,6 +707,8 @@ async def _async_finish_flow( 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']) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2ccb67104a0c2..a6b97855c10d3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -579,8 +579,7 @@ async def test_update_entry_no_change_does_not_call_save(manager): with patch('homeassistant.config_entries' '.ConfigEntries._async_schedule_save') as mock_save: manager.async_update_entry(entry) - - assert not mock_save.mock_calls + assert not mock_save.mock_calls async def test_setup_raise_not_ready(hass, caplog): From 16f712aa99c971fe269529a137838f15c8225925 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 18:12:42 +0100 Subject: [PATCH 34/47] Fix line too long --- homeassistant/config_entries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1d171812d3b35..299dcfc6826e4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,8 @@ class ConfigEntry: '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, + 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.""" From 2020edfc97ac4bd4532669984fee87f2353c2e22 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 21:53:53 +0100 Subject: [PATCH 35/47] No 3.6 typing and ignore type issue instead --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 299dcfc6826e4..a1d8fd0b8bf6b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -259,7 +259,7 @@ def __init__(self, version: str, domain: str, title: str, data: dict, self.state = state # Listeners to call on update - self.update_listeners: list = [] + self.update_listeners = [] # type: ignore # Function to cancel a scheduled retry self._async_cancel_retry_setup = None From 2d361698df1bfd42c09c8133171553bdb44af069 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 22:03:55 +0100 Subject: [PATCH 36/47] Permission should be edit and not add --- .../components/config/config_entries.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 6bafcc24f5fb5..6fe54b12bd986 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -172,7 +172,7 @@ async def post(self, request): """ if not request['hass_user'].is_admin: raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='add') + perm_category=CAT_CONFIG_ENTRIES, permission='edit') # pylint: disable=no-value-for-parameter return await super().post(request) @@ -183,3 +183,21 @@ class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): 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) From 728c7a4429dbce051d05828e01c04cb6644fe692 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 22:05:06 +0100 Subject: [PATCH 37/47] Define type in comment --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a1d8fd0b8bf6b..ee71c40525e30 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -259,7 +259,7 @@ def __init__(self, version: str, domain: str, title: str, data: dict, self.state = state # Listeners to call on update - self.update_listeners = [] # type: ignore + self.update_listeners = [] # type: list # Function to cancel a scheduled retry self._async_cancel_retry_setup = None From 82521171ae3b133338df5c051c6d2d6f041165e3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Feb 2019 22:18:40 +0100 Subject: [PATCH 38/47] Inline supports_options --- homeassistant/components/config/config_entries.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 6fe54b12bd986..65f65cbcec553 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -52,11 +52,6 @@ async def get(self, request): """List available config entries.""" hass = request.app['hass'] - def supports_options(domain): - """Check if config entry supports options.""" - return hasattr( - config_entries.HANDLERS[domain], 'async_get_options_flow') - return self.json([{ 'entry_id': entry.entry_id, 'domain': entry.domain, @@ -64,7 +59,9 @@ def supports_options(domain): 'source': entry.source, 'state': entry.state, 'connection_class': entry.connection_class, - 'supports_options': supports_options(entry.domain), + 'supports_options': hasattr( + config_entries.HANDLERS[entry.domain], + 'async_get_options_flow'), } for entry in hass.config_entries.async_entries()]) From baae873424a8e24f20d60993ba5ca1e62350068e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 18 Feb 2019 20:24:59 +0100 Subject: [PATCH 39/47] Fix tests --- homeassistant/config_entries.py | 14 ++++++-------- tests/test_config_entries.py | 14 +------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ee71c40525e30..552fd142dd167 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -528,20 +528,18 @@ async def async_load(self) -> None: def async_update_entry( self, entry: ConfigEntry, *, data: Optional[dict] = _UNDEF, - options: Optional[dict] = None) -> None: + options: Optional[dict] = _UNDEF) -> None: """Update a config entry.""" - if data is _UNDEF and not options: - return - if data is not _UNDEF: entry.data = data - if options: + if options is not _UNDEF: entry.options = options - for listener_ref in entry.update_listeners: - listener = listener_ref() - self.hass.async_create_task(listener(self.hass, entry)) + 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() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a6b97855c10d3..b56c79d42e195 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -553,6 +553,7 @@ async def test_update_entry_options_and_trigger_listener(hass, manager): ) entry.add_to_manager(manager) + @callback def update_listener(hass, entry): """Test function.""" hass.data['update_listener'] = True @@ -569,19 +570,6 @@ def update_listener(hass, entry): assert hass.data['update_listener'] is True -async def test_update_entry_no_change_does_not_call_save(manager): - """Test that we only call save when new data or options are input.""" - entry = MockConfigEntry( - domain='test', - ) - entry.add_to_manager(manager) - - with patch('homeassistant.config_entries' - '.ConfigEntries._async_schedule_save') as mock_save: - manager.async_update_entry(entry) - assert not mock_save.mock_calls - - async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain='test') From b233b6b8db601381e21892a7ca80b8ad133447bc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 17:49:24 +0100 Subject: [PATCH 40/47] Options shouldn't be None --- homeassistant/config_entries.py | 3 +-- tests/common.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 552fd142dd167..d09dde2e8aff7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,8 +226,7 @@ class ConfigEntry: '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, + source: str, connection_class: str, options: dict = {}, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" diff --git a/tests/common.py b/tests/common.py index c9f60423d2231..475dc50bf07d3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -608,7 +608,7 @@ 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, options=None, + state=None, options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN): """Initialize a mock config entry.""" kwargs = { From a7bf0a22aeae4e55baaeeb0ad7d3e6d64093267c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 17:51:12 +0100 Subject: [PATCH 41/47] Rename Options to OptionsFlowManager --- homeassistant/config_entries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d09dde2e8aff7..86f99825db666 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -435,7 +435,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 = Options(hass) + self.options = OptionsFlowManager(hass) self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -674,7 +674,7 @@ def _async_in_progress(self): flw['flow_id'] != self.flow_id] -class Options: +class OptionsFlowManager: """Flow to set options for a configuration entry.""" def __init__(self, hass: HomeAssistant) -> None: From 4179a4bad36e33918b7902e33e669cafcfeb2d0e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 20:07:58 +0100 Subject: [PATCH 42/47] Options flow doesn't care about context --- homeassistant/config_entries.py | 1 - tests/components/config/test_config_entries.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 86f99825db666..c0a938d5222fc 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -695,7 +695,6 @@ async def _async_create_flow( return flow = HANDLERS[entry.domain].async_get_options_flow( entry.data, entry.options) - flow.init_step = context['source'] return flow async def _async_finish_flow( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 4dd1f3f46bca4..77a35757f6001 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -495,7 +495,7 @@ def __init__(self, config, options): self.config = config self.options = options - async def async_step_user(self, user_input=None): + async def async_step_init(self, user_input=None): schema = OrderedDict() schema[vol.Required('enabled')] = bool return self.async_show_form( @@ -557,7 +557,7 @@ def __init__(self, config, options): self.config = config self.options = options - async def async_step_user(self, user_input=None): + async def async_step_init(self, user_input=None): return self.async_show_form( step_id='finish', data_schema=vol.Schema({ From 1206207c38420e8c204eb4c4d1d47c7a8bc034f8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 20:08:43 +0100 Subject: [PATCH 43/47] Remove stale print --- tests/test_config_entries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b56c79d42e195..b4dff5b46e159 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -635,7 +635,6 @@ def __init__(self, config, options): return OptionsFlowHandler(config, options) config_entries.HANDLERS['test'] = TestFlow() - print(entry) flow = await manager.options._async_create_flow( entry.entry_id, context={'source': 'test'}, data=None) From a07d851f3383de94fcbce46507b7e67cdec448c5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 20:41:00 +0100 Subject: [PATCH 44/47] Fix tests --- tests/components/config/test_config_entries.py | 1 - tests/test_config_entries.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 77a35757f6001..87ed83d9a7ef3 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -523,7 +523,6 @@ async def async_step_init(self, user_input=None): data = await resp.json() data.pop('flow_id') - print(data) assert data == { 'type': 'form', 'handler': 'test1', diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b4dff5b46e159..8991035cc225f 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -553,10 +553,11 @@ async def test_update_entry_options_and_trigger_listener(hass, manager): ) entry.add_to_manager(manager) - @callback - def update_listener(hass, entry): + async def update_listener(hass, entry): """Test function.""" - hass.data['update_listener'] = True + assert entry.options == { + 'second': True + } entry.add_update_listener(update_listener) @@ -567,7 +568,6 @@ def update_listener(hass, entry): assert entry.options == { 'second': True } - assert hass.data['update_listener'] is True async def test_setup_raise_not_ready(hass, caplog): From 942d4c7a014fb81e31494631d1a0d6fbbc419ae1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Feb 2019 20:55:55 +0100 Subject: [PATCH 45/47] Martin is right --- homeassistant/config_entries.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c0a938d5222fc..fdbc009e4655d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -226,7 +226,8 @@ class ConfigEntry: 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, - source: str, connection_class: str, options: 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.""" @@ -246,7 +247,7 @@ def __init__(self, version: str, domain: str, title: str, data: dict, self.data = data # Entry options - self.options = options + self.options = options or {} # Source of the configuration (user, discovery, cloud) self.source = source From 9c2ae552879352e6be24eebc65eaa62a6fef5549 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 21 Feb 2019 18:15:07 +0100 Subject: [PATCH 46/47] Fix typing issues --- homeassistant/config_entries.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fdbc009e4655d..bd936184f6619 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -525,10 +525,7 @@ async def async_load(self) -> None: for entry in config['entries']] @callback - def async_update_entry( - self, entry: ConfigEntry, *, - data: Optional[dict] = _UNDEF, - options: Optional[dict] = _UNDEF) -> None: + def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -684,9 +681,7 @@ def __init__(self, hass: HomeAssistant) -> None: self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) - async def _async_create_flow( - self, entry_id: str, *, - context: dict, data) -> data_entry_flow.FlowHandler: + 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. @@ -698,8 +693,7 @@ async def _async_create_flow( entry.data, entry.options) return flow - async def _async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict): + 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. From ccf2b155f06d5c4ada22104929ad985dd43bfd1a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 21 Feb 2019 23:23:25 +0100 Subject: [PATCH 47/47] Update added comment in code --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bd936184f6619..f287820dc8749 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -520,7 +520,7 @@ async def async_load(self) -> None: # New in 0.79 connection_class=entry.get('connection_class', CONN_CLASS_UNKNOWN), - # New in 0.8x + # New in 0.89 options=entry.get('options')) for entry in config['entries']]