182 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Python
		
	
	
"""Config flow for RSS/Atom feeds."""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import logging
 | 
						|
from typing import TYPE_CHECKING, Any
 | 
						|
import urllib.error
 | 
						|
 | 
						|
import feedparser
 | 
						|
import voluptuous as vol
 | 
						|
 | 
						|
from homeassistant.config_entries import (
 | 
						|
    SOURCE_IMPORT,
 | 
						|
    ConfigEntry,
 | 
						|
    ConfigFlow,
 | 
						|
    ConfigFlowResult,
 | 
						|
    OptionsFlow,
 | 
						|
    OptionsFlowWithConfigEntry,
 | 
						|
)
 | 
						|
from homeassistant.const import CONF_URL
 | 
						|
from homeassistant.core import HomeAssistant, callback
 | 
						|
import homeassistant.helpers.config_validation as cv
 | 
						|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 | 
						|
from homeassistant.helpers.selector import (
 | 
						|
    TextSelector,
 | 
						|
    TextSelectorConfig,
 | 
						|
    TextSelectorType,
 | 
						|
)
 | 
						|
from homeassistant.util import slugify
 | 
						|
 | 
						|
from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DOMAIN
 | 
						|
 | 
						|
LOGGER = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
async def async_fetch_feed(hass: HomeAssistant, url: str) -> feedparser.FeedParserDict:
 | 
						|
    """Fetch the feed."""
 | 
						|
    return await hass.async_add_executor_job(feedparser.parse, url)
 | 
						|
 | 
						|
 | 
						|
class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
 | 
						|
    """Handle a config flow."""
 | 
						|
 | 
						|
    VERSION = 1
 | 
						|
    _config_entry: ConfigEntry
 | 
						|
    _max_entries: int | None = None
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    @callback
 | 
						|
    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
 | 
						|
        """Get the options flow for this handler."""
 | 
						|
        return FeedReaderOptionsFlowHandler(config_entry)
 | 
						|
 | 
						|
    def show_user_form(
 | 
						|
        self,
 | 
						|
        user_input: dict[str, Any] | None = None,
 | 
						|
        errors: dict[str, str] | None = None,
 | 
						|
        description_placeholders: dict[str, str] | None = None,
 | 
						|
        step_id: str = "user",
 | 
						|
    ) -> ConfigFlowResult:
 | 
						|
        """Show the user form."""
 | 
						|
        if user_input is None:
 | 
						|
            user_input = {}
 | 
						|
        return self.async_show_form(
 | 
						|
            step_id=step_id,
 | 
						|
            data_schema=vol.Schema(
 | 
						|
                {
 | 
						|
                    vol.Required(
 | 
						|
                        CONF_URL, default=user_input.get(CONF_URL, "")
 | 
						|
                    ): TextSelector(TextSelectorConfig(type=TextSelectorType.URL))
 | 
						|
                }
 | 
						|
            ),
 | 
						|
            description_placeholders=description_placeholders,
 | 
						|
            errors=errors,
 | 
						|
        )
 | 
						|
 | 
						|
    def abort_on_import_error(self, url: str, error: str) -> ConfigFlowResult:
 | 
						|
        """Abort import flow on error."""
 | 
						|
        async_create_issue(
 | 
						|
            self.hass,
 | 
						|
            DOMAIN,
 | 
						|
            f"import_yaml_error_{DOMAIN}_{error}_{slugify(url)}",
 | 
						|
            breaks_in_ha_version="2025.1.0",
 | 
						|
            is_fixable=False,
 | 
						|
            issue_domain=DOMAIN,
 | 
						|
            severity=IssueSeverity.WARNING,
 | 
						|
            translation_key=f"import_yaml_error_{error}",
 | 
						|
            translation_placeholders={"url": url},
 | 
						|
        )
 | 
						|
        return self.async_abort(reason=error)
 | 
						|
 | 
						|
    async def async_step_user(
 | 
						|
        self, user_input: dict[str, Any] | None = None
 | 
						|
    ) -> ConfigFlowResult:
 | 
						|
        """Handle a flow initialized by the user."""
 | 
						|
        if not user_input:
 | 
						|
            return self.show_user_form()
 | 
						|
 | 
						|
        self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
 | 
						|
 | 
						|
        feed = await async_fetch_feed(self.hass, user_input[CONF_URL])
 | 
						|
 | 
						|
        if feed.bozo:
 | 
						|
            LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception)
 | 
						|
            if isinstance(feed.bozo_exception, urllib.error.URLError):
 | 
						|
                if self.context["source"] == SOURCE_IMPORT:
 | 
						|
                    return self.abort_on_import_error(user_input[CONF_URL], "url_error")
 | 
						|
                return self.show_user_form(user_input, {"base": "url_error"})
 | 
						|
 | 
						|
        feed_title = feed["feed"]["title"]
 | 
						|
 | 
						|
        return self.async_create_entry(
 | 
						|
            title=feed_title,
 | 
						|
            data=user_input,
 | 
						|
            options={CONF_MAX_ENTRIES: self._max_entries or DEFAULT_MAX_ENTRIES},
 | 
						|
        )
 | 
						|
 | 
						|
    async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
 | 
						|
        """Handle an import flow."""
 | 
						|
        self._max_entries = user_input[CONF_MAX_ENTRIES]
 | 
						|
        return await self.async_step_user({CONF_URL: user_input[CONF_URL]})
 | 
						|
 | 
						|
    async def async_step_reconfigure(
 | 
						|
        self, _: dict[str, Any] | None = None
 | 
						|
    ) -> ConfigFlowResult:
 | 
						|
        """Handle a reconfiguration flow initialized by the user."""
 | 
						|
        config_entry = self.hass.config_entries.async_get_entry(
 | 
						|
            self.context["entry_id"]
 | 
						|
        )
 | 
						|
        if TYPE_CHECKING:
 | 
						|
            assert config_entry is not None
 | 
						|
        self._config_entry = config_entry
 | 
						|
        return await self.async_step_reconfigure_confirm()
 | 
						|
 | 
						|
    async def async_step_reconfigure_confirm(
 | 
						|
        self, user_input: dict[str, Any] | None = None
 | 
						|
    ) -> ConfigFlowResult:
 | 
						|
        """Handle a reconfiguration flow initialized by the user."""
 | 
						|
        if not user_input:
 | 
						|
            return self.show_user_form(
 | 
						|
                user_input={**self._config_entry.data},
 | 
						|
                description_placeholders={"name": self._config_entry.title},
 | 
						|
                step_id="reconfigure_confirm",
 | 
						|
            )
 | 
						|
 | 
						|
        feed = await async_fetch_feed(self.hass, user_input[CONF_URL])
 | 
						|
 | 
						|
        if feed.bozo:
 | 
						|
            LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception)
 | 
						|
            if isinstance(feed.bozo_exception, urllib.error.URLError):
 | 
						|
                return self.show_user_form(
 | 
						|
                    user_input=user_input,
 | 
						|
                    description_placeholders={"name": self._config_entry.title},
 | 
						|
                    step_id="reconfigure_confirm",
 | 
						|
                    errors={"base": "url_error"},
 | 
						|
                )
 | 
						|
 | 
						|
        self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
 | 
						|
        return self.async_abort(reason="reconfigure_successful")
 | 
						|
 | 
						|
 | 
						|
class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry):
 | 
						|
    """Handle an options flow."""
 | 
						|
 | 
						|
    async def async_step_init(
 | 
						|
        self, user_input: dict[str, Any] | None = None
 | 
						|
    ) -> ConfigFlowResult:
 | 
						|
        """Handle options flow."""
 | 
						|
 | 
						|
        if user_input is not None:
 | 
						|
            return self.async_create_entry(title="", data=user_input)
 | 
						|
 | 
						|
        data_schema = vol.Schema(
 | 
						|
            {
 | 
						|
                vol.Optional(
 | 
						|
                    CONF_MAX_ENTRIES,
 | 
						|
                    default=self.options.get(CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES),
 | 
						|
                ): cv.positive_int,
 | 
						|
            }
 | 
						|
        )
 | 
						|
        return self.async_show_form(step_id="init", data_schema=data_schema)
 |