Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure forked-daapd devices."""
2 
3 from contextlib import suppress
4 import logging
5 from typing import Any
6 
7 from pyforked_daapd import ForkedDaapdAPI
8 import voluptuous as vol
9 
10 from homeassistant.components import zeroconf
11 from homeassistant.config_entries import (
12  ConfigEntry,
13  ConfigFlow,
14  ConfigFlowResult,
15  OptionsFlow,
16 )
17 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
18 from homeassistant.core import callback
19 from homeassistant.helpers.aiohttp_client import async_get_clientsession
20 
21 from .const import (
22  CONF_LIBRESPOT_JAVA_PORT,
23  CONF_MAX_PLAYLISTS,
24  CONF_TTS_PAUSE_TIME,
25  CONF_TTS_VOLUME,
26  DEFAULT_PORT,
27  DEFAULT_TTS_PAUSE_TIME,
28  DEFAULT_TTS_VOLUME,
29  DOMAIN,
30 )
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 
35 # Can't use all vol types: https://github.com/home-assistant/core/issues/32819
36 DATA_SCHEMA_DICT = {
37  vol.Required(CONF_HOST): str,
38  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
39  vol.Optional(CONF_PASSWORD, default=""): str,
40 }
41 
42 TEST_CONNECTION_ERROR_DICT = {
43  "forbidden": "forbidden",
44  "ok": "ok",
45  "websocket_not_enabled": "websocket_not_enabled",
46  "wrong_host_or_port": "wrong_host_or_port",
47  "wrong_password": "wrong_password",
48  "wrong_server_type": "wrong_server_type",
49 }
50 
51 
53  """Handle a forked-daapd options flow."""
54 
55  async def async_step_init(
56  self, user_input: dict[str, Any] | None = None
57  ) -> ConfigFlowResult:
58  """Manage the options."""
59  if user_input is not None:
60  return self.async_create_entryasync_create_entry(title="options", data=user_input)
61 
62  return self.async_show_formasync_show_form(
63  step_id="init",
64  data_schema=vol.Schema(
65  {
66  vol.Optional(
67  CONF_TTS_PAUSE_TIME,
68  default=self.config_entryconfig_entryconfig_entry.options.get(
69  CONF_TTS_PAUSE_TIME, DEFAULT_TTS_PAUSE_TIME
70  ),
71  ): float,
72  vol.Optional(
73  CONF_TTS_VOLUME,
74  default=self.config_entryconfig_entryconfig_entry.options.get(
75  CONF_TTS_VOLUME, DEFAULT_TTS_VOLUME
76  ),
77  ): float,
78  vol.Optional(
79  CONF_LIBRESPOT_JAVA_PORT,
80  default=self.config_entryconfig_entryconfig_entry.options.get(
81  CONF_LIBRESPOT_JAVA_PORT, 24879
82  ),
83  ): int,
84  vol.Optional(
85  CONF_MAX_PLAYLISTS,
86  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_MAX_PLAYLISTS, 10),
87  ): int,
88  }
89  ),
90  )
91 
92 
93 def fill_in_schema_dict(some_input):
94  """Fill in schema dict defaults from user_input."""
95  schema_dict = {}
96  for field, _type in DATA_SCHEMA_DICT.items():
97  if some_input.get(str(field)):
98  schema_dict[vol.Optional(str(field), default=some_input[str(field)])] = (
99  _type
100  )
101  else:
102  schema_dict[field] = _type
103  return schema_dict
104 
105 
106 class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN):
107  """Handle a forked-daapd config flow."""
108 
109  VERSION = 1
110 
111  def __init__(self) -> None:
112  """Initialize."""
113  self.discovery_schemadiscovery_schema: vol.Schema | None = None
114 
115  @staticmethod
116  @callback
118  config_entry: ConfigEntry,
119  ) -> ForkedDaapdOptionsFlowHandler:
120  """Return options flow handler."""
122 
123  async def validate_input(self, user_input):
124  """Validate the user input."""
125  websession = async_get_clientsession(self.hass)
126  validate_result = await ForkedDaapdAPI.test_connection(
127  websession=websession,
128  host=user_input[CONF_HOST],
129  port=user_input[CONF_PORT],
130  password=user_input[CONF_PASSWORD],
131  )
132  validate_result[0] = TEST_CONNECTION_ERROR_DICT.get(
133  validate_result[0], "unknown_error"
134  )
135  return validate_result
136 
137  async def async_step_user(
138  self, user_input: dict[str, Any] | None = None
139  ) -> ConfigFlowResult:
140  """Handle a forked-daapd config flow start.
141 
142  Manage device specific parameters.
143  """
144  if user_input is not None:
145  # check for any entries with same host, abort if found
146  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
147  validate_result = await self.validate_inputvalidate_input(user_input)
148  if validate_result[0] == "ok": # success
149  _LOGGER.debug("Connected successfully. Creating entry")
150  return self.async_create_entryasync_create_entryasync_create_entry(
151  title=validate_result[1], data=user_input
152  )
153  return self.async_show_formasync_show_formasync_show_form(
154  step_id="user",
155  data_schema=vol.Schema(fill_in_schema_dict(user_input)),
156  errors={"base": validate_result[0]},
157  )
158  if self.discovery_schemadiscovery_schema: # stop at form to allow user to set up manually
159  return self.async_show_formasync_show_formasync_show_form(
160  step_id="user", data_schema=self.discovery_schemadiscovery_schema, errors={}
161  )
162  return self.async_show_formasync_show_formasync_show_form(
163  step_id="user", data_schema=vol.Schema(DATA_SCHEMA_DICT), errors={}
164  )
165 
167  self, discovery_info: zeroconf.ZeroconfServiceInfo
168  ) -> ConfigFlowResult:
169  """Prepare configuration for a discovered forked-daapd device."""
170  version_num = 0
171  zeroconf_properties = discovery_info.properties
172  if zeroconf_properties.get("Machine Name"):
173  with suppress(ValueError):
174  version_num = int(
175  zeroconf_properties.get("mtd-version", "0").split(".")[0]
176  )
177  if version_num < 27:
178  return self.async_abortasync_abortasync_abort(reason="not_forked_daapd")
179  await self.async_set_unique_idasync_set_unique_id(zeroconf_properties["Machine Name"])
180  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
181 
182  # Update title and abort if we already have an entry for this host
183  for entry in self._async_current_entries_async_current_entries():
184  if entry.data.get(CONF_HOST) != discovery_info.host:
185  continue
186  self.hass.config_entries.async_update_entry(
187  entry,
188  title=zeroconf_properties["Machine Name"],
189  )
190  return self.async_abortasync_abortasync_abort(reason="already_configured")
191 
192  zeroconf_data = {
193  CONF_HOST: discovery_info.host,
194  CONF_PORT: discovery_info.port,
195  CONF_NAME: zeroconf_properties["Machine Name"],
196  }
197  self.discovery_schemadiscovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data))
198  self.context.update({"title_placeholders": zeroconf_data})
199  return await self.async_step_userasync_step_userasync_step_user()
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:139
ForkedDaapdOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:119
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:168
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:57
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)