Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for RSS/Atom feeds."""
2 
3 from __future__ import annotations
4 
5 import html
6 import logging
7 from typing import Any
8 import urllib.error
9 
10 import feedparser
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import (
14  SOURCE_IMPORT,
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import CONF_URL
21 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
25  TextSelector,
26  TextSelectorConfig,
27  TextSelectorType,
28 )
29 from homeassistant.util import slugify
30 
31 from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DOMAIN
32 
33 LOGGER = logging.getLogger(__name__)
34 
35 
36 async def async_fetch_feed(hass: HomeAssistant, url: str) -> feedparser.FeedParserDict:
37  """Fetch the feed."""
38  return await hass.async_add_executor_job(feedparser.parse, url)
39 
40 
41 class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
42  """Handle a config flow."""
43 
44  VERSION = 1
45  _max_entries: int | None = None
46 
47  @staticmethod
48  @callback
50  config_entry: ConfigEntry,
51  ) -> OptionsFlow:
52  """Get the options flow for this handler."""
54 
56  self,
57  user_input: dict[str, Any] | None = None,
58  errors: dict[str, str] | None = None,
59  description_placeholders: dict[str, str] | None = None,
60  step_id: str = "user",
61  ) -> ConfigFlowResult:
62  """Show the user form."""
63  if user_input is None:
64  user_input = {}
65  return self.async_show_formasync_show_formasync_show_form(
66  step_id=step_id,
67  data_schema=vol.Schema(
68  {
69  vol.Required(
70  CONF_URL, default=user_input.get(CONF_URL, "")
71  ): TextSelector(TextSelectorConfig(type=TextSelectorType.URL))
72  }
73  ),
74  description_placeholders=description_placeholders,
75  errors=errors,
76  )
77 
78  def abort_on_import_error(self, url: str, error: str) -> ConfigFlowResult:
79  """Abort import flow on error."""
81  self.hass,
82  DOMAIN,
83  f"import_yaml_error_{DOMAIN}_{error}_{slugify(url)}",
84  breaks_in_ha_version="2025.1.0",
85  is_fixable=False,
86  issue_domain=DOMAIN,
87  severity=IssueSeverity.WARNING,
88  translation_key=f"import_yaml_error_{error}",
89  translation_placeholders={"url": url},
90  )
91  return self.async_abortasync_abortasync_abort(reason=error)
92 
93  async def async_step_user(
94  self, user_input: dict[str, Any] | None = None
95  ) -> ConfigFlowResult:
96  """Handle a flow initialized by the user."""
97  if not user_input:
98  return self.show_user_formshow_user_form()
99 
100  self._async_abort_entries_match_async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
101 
102  feed = await async_fetch_feed(self.hass, user_input[CONF_URL])
103 
104  if feed.bozo:
105  LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception)
106  if isinstance(feed.bozo_exception, urllib.error.URLError):
107  if self.context["source"] == SOURCE_IMPORT:
108  return self.abort_on_import_errorabort_on_import_error(user_input[CONF_URL], "url_error")
109  return self.show_user_formshow_user_form(user_input, {"base": "url_error"})
110 
111  feed_title = html.unescape(feed["feed"]["title"])
112 
113  return self.async_create_entryasync_create_entryasync_create_entry(
114  title=feed_title,
115  data=user_input,
116  options={CONF_MAX_ENTRIES: self._max_entries_max_entries or DEFAULT_MAX_ENTRIES},
117  )
118 
119  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
120  """Handle an import flow."""
121  self._max_entries_max_entries = import_data[CONF_MAX_ENTRIES]
122  return await self.async_step_userasync_step_userasync_step_user({CONF_URL: import_data[CONF_URL]})
123 
125  self, user_input: dict[str, Any] | None = None
126  ) -> ConfigFlowResult:
127  """Handle a reconfiguration flow initialized by the user."""
128  reconfigure_entry = self._get_reconfigure_entry_get_reconfigure_entry()
129  if not user_input:
130  return self.show_user_formshow_user_form(
131  user_input={**reconfigure_entry.data},
132  description_placeholders={"name": reconfigure_entry.title},
133  step_id="reconfigure",
134  )
135 
136  feed = await async_fetch_feed(self.hass, user_input[CONF_URL])
137 
138  if feed.bozo:
139  LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception)
140  if isinstance(feed.bozo_exception, urllib.error.URLError):
141  return self.show_user_formshow_user_form(
142  user_input=user_input,
143  description_placeholders={"name": reconfigure_entry.title},
144  step_id="reconfigure",
145  errors={"base": "url_error"},
146  )
147 
148  self.hass.config_entries.async_update_entry(reconfigure_entry, data=user_input)
149  return self.async_abortasync_abortasync_abort(reason="reconfigure_successful")
150 
151 
153  """Handle an options flow."""
154 
155  async def async_step_init(
156  self, user_input: dict[str, Any] | None = None
157  ) -> ConfigFlowResult:
158  """Handle options flow."""
159 
160  if user_input is not None:
161  return self.async_create_entryasync_create_entry(title="", data=user_input)
162 
163  data_schema = vol.Schema(
164  {
165  vol.Optional(
166  CONF_MAX_ENTRIES,
167  default=self.config_entryconfig_entryconfig_entry.options.get(
168  CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES
169  ),
170  ): cv.positive_int,
171  }
172  )
173  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:51
ConfigFlowResult abort_on_import_error(self, str url, str error)
Definition: config_flow.py:78
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:95
ConfigFlowResult async_step_reconfigure(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:126
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:119
ConfigFlowResult show_user_form(self, dict[str, Any]|None user_input=None, dict[str, str]|None errors=None, dict[str, str]|None description_placeholders=None, str step_id="user")
Definition: config_flow.py:61
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:157
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
feedparser.FeedParserDict async_fetch_feed(HomeAssistant hass, str url)
Definition: config_flow.py:36
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69