Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the MJPEG IP Camera integration."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 from types import MappingProxyType
7 from typing import Any
8 
9 import requests
10 from requests.auth import HTTPBasicAuth, HTTPDigestAuth
11 from requests.exceptions import HTTPError, Timeout
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import (
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import (
21  CONF_AUTHENTICATION,
22  CONF_NAME,
23  CONF_PASSWORD,
24  CONF_USERNAME,
25  CONF_VERIFY_SSL,
26  HTTP_BASIC_AUTHENTICATION,
27  HTTP_DIGEST_AUTHENTICATION,
28 )
29 from homeassistant.core import HomeAssistant, callback
30 from homeassistant.exceptions import HomeAssistantError
31 
32 from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN, LOGGER
33 
34 
35 @callback
37  defaults: dict[str, Any] | MappingProxyType[str, Any], show_name: bool = False
38 ) -> vol.Schema:
39  """Return MJPEG IP Camera schema."""
40  schema = {
41  vol.Required(CONF_MJPEG_URL, default=defaults.get(CONF_MJPEG_URL)): str,
42  vol.Optional(
43  CONF_STILL_IMAGE_URL,
44  description={"suggested_value": defaults.get(CONF_STILL_IMAGE_URL)},
45  ): str,
46  vol.Optional(
47  CONF_USERNAME,
48  description={"suggested_value": defaults.get(CONF_USERNAME)},
49  ): str,
50  vol.Optional(
51  CONF_PASSWORD,
52  default=defaults.get(CONF_PASSWORD, ""),
53  ): str,
54  vol.Optional(
55  CONF_VERIFY_SSL,
56  default=defaults.get(CONF_VERIFY_SSL, True),
57  ): bool,
58  }
59 
60  if show_name:
61  schema = {
62  vol.Required(CONF_NAME, default=defaults.get(CONF_NAME)): str,
63  **schema,
64  }
65 
66  return vol.Schema(schema)
67 
68 
70  url: str,
71  username: str | None,
72  password: str,
73  verify_ssl: bool,
74  authentication: str = HTTP_BASIC_AUTHENTICATION,
75 ) -> str:
76  """Test if the given setting works as expected."""
77  auth: HTTPDigestAuth | HTTPBasicAuth | None = None
78  if username and password:
79  if authentication == HTTP_DIGEST_AUTHENTICATION:
80  auth = HTTPDigestAuth(username, password)
81  else:
82  auth = HTTPBasicAuth(username, password)
83 
84  response = requests.get(
85  url,
86  auth=auth,
87  stream=True,
88  timeout=10,
89  verify=verify_ssl,
90  )
91 
92  if response.status_code == HTTPStatus.UNAUTHORIZED:
93  # If unauthorized, try again using digest auth
94  if authentication == HTTP_BASIC_AUTHENTICATION:
95  return validate_url(
96  url, username, password, verify_ssl, HTTP_DIGEST_AUTHENTICATION
97  )
98  raise InvalidAuth
99 
100  response.raise_for_status()
101  response.close()
102 
103  return authentication
104 
105 
107  hass: HomeAssistant, user_input: dict[str, Any]
108 ) -> tuple[dict[str, str], str]:
109  """Manage MJPEG IP Camera options."""
110  errors = {}
111  field = "base"
112  authentication = HTTP_BASIC_AUTHENTICATION
113  try:
114  for field in (CONF_MJPEG_URL, CONF_STILL_IMAGE_URL):
115  if not (url := user_input.get(field)):
116  continue
117  authentication = await hass.async_add_executor_job(
118  validate_url,
119  url,
120  user_input.get(CONF_USERNAME),
121  user_input[CONF_PASSWORD],
122  user_input[CONF_VERIFY_SSL],
123  )
124  except InvalidAuth:
125  errors["username"] = "invalid_auth"
126  except (OSError, HTTPError, Timeout):
127  LOGGER.exception("Cannot connect to %s", user_input[CONF_MJPEG_URL])
128  errors[field] = "cannot_connect"
129 
130  return (errors, authentication)
131 
132 
133 class MJPEGFlowHandler(ConfigFlow, domain=DOMAIN):
134  """Config flow for MJPEG IP Camera."""
135 
136  VERSION = 1
137 
138  @staticmethod
139  @callback
141  config_entry: ConfigEntry,
142  ) -> MJPEGOptionsFlowHandler:
143  """Get the options flow for this handler."""
144  return MJPEGOptionsFlowHandler()
145 
146  async def async_step_user(
147  self, user_input: dict[str, Any] | None = None
148  ) -> ConfigFlowResult:
149  """Handle a flow initialized by the user."""
150  errors: dict[str, str] = {}
151 
152  if user_input is not None:
153  errors, authentication = await async_validate_input(self.hass, user_input)
154  if not errors:
155  self._async_abort_entries_match_async_abort_entries_match(
156  {CONF_MJPEG_URL: user_input[CONF_MJPEG_URL]}
157  )
158 
159  # Storing data in option, to allow for changing them later
160  # using an options flow.
161  return self.async_create_entryasync_create_entryasync_create_entry(
162  title=user_input.get(CONF_NAME, user_input[CONF_MJPEG_URL]),
163  data={},
164  options={
165  CONF_AUTHENTICATION: authentication,
166  CONF_MJPEG_URL: user_input[CONF_MJPEG_URL],
167  CONF_PASSWORD: user_input[CONF_PASSWORD],
168  CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
169  CONF_USERNAME: user_input.get(CONF_USERNAME),
170  CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
171  },
172  )
173  else:
174  user_input = {}
175 
176  return self.async_show_formasync_show_formasync_show_form(
177  step_id="user",
178  data_schema=async_get_schema(user_input, show_name=True),
179  errors=errors,
180  )
181 
182 
184  """Handle MJPEG IP Camera options."""
185 
186  async def async_step_init(
187  self, user_input: dict[str, Any] | None = None
188  ) -> ConfigFlowResult:
189  """Manage MJPEG IP Camera options."""
190  errors: dict[str, str] = {}
191 
192  if user_input is not None:
193  errors, authentication = await async_validate_input(self.hass, user_input)
194  if not errors:
195  for entry in self.hass.config_entries.async_entries(DOMAIN):
196  if (
197  entry.entry_id != self.config_entryconfig_entryconfig_entry.entry_id
198  and entry.options[CONF_MJPEG_URL] == user_input[CONF_MJPEG_URL]
199  ):
200  errors = {CONF_MJPEG_URL: "already_configured"}
201 
202  if not errors:
203  return self.async_create_entryasync_create_entry(
204  title=user_input.get(CONF_NAME, user_input[CONF_MJPEG_URL]),
205  data={
206  CONF_AUTHENTICATION: authentication,
207  CONF_MJPEG_URL: user_input[CONF_MJPEG_URL],
208  CONF_PASSWORD: user_input[CONF_PASSWORD],
209  CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
210  CONF_USERNAME: user_input.get(CONF_USERNAME),
211  CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
212  },
213  )
214  else:
215  user_input = {}
216 
217  return self.async_show_formasync_show_form(
218  step_id="init",
219  data_schema=async_get_schema(user_input or self.config_entryconfig_entryconfig_entry.options),
220  errors=errors,
221  )
222 
223 
225  """Error to indicate there is invalid auth."""
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:148
MJPEGOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:142
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:188
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)
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)
tuple[dict[str, str], str] async_validate_input(HomeAssistant hass, dict[str, Any] user_input)
Definition: config_flow.py:108
vol.Schema async_get_schema(dict[str, Any]|MappingProxyType[str, Any] defaults, bool show_name=False)
Definition: config_flow.py:38
str validate_url(str url, str|None username, str password, bool verify_ssl, str authentication=HTTP_BASIC_AUTHENTICATION)
Definition: config_flow.py:75