Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Adds config flow for Workday integration."""
2 
3 from __future__ import annotations
4 
5 from functools import partial
6 from typing import Any
7 
8 from holidays import PUBLIC, HolidayBase, country_holidays, list_supported_countries
9 import voluptuous as vol
10 
11 from homeassistant.config_entries import (
12  ConfigEntry,
13  ConfigFlow,
14  ConfigFlowResult,
15  OptionsFlow,
16 )
17 from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME
18 from homeassistant.core import callback
19 from homeassistant.data_entry_flow import AbortFlow
20 from homeassistant.exceptions import HomeAssistantError
22  CountrySelector,
23  CountrySelectorConfig,
24  LanguageSelector,
25  LanguageSelectorConfig,
26  NumberSelector,
27  NumberSelectorConfig,
28  NumberSelectorMode,
29  SelectSelector,
30  SelectSelectorConfig,
31  SelectSelectorMode,
32  TextSelector,
33 )
34 from homeassistant.util import dt as dt_util
35 
36 from .const import (
37  ALLOWED_DAYS,
38  CONF_ADD_HOLIDAYS,
39  CONF_CATEGORY,
40  CONF_EXCLUDES,
41  CONF_OFFSET,
42  CONF_PROVINCE,
43  CONF_REMOVE_HOLIDAYS,
44  CONF_WORKDAYS,
45  DEFAULT_EXCLUDES,
46  DEFAULT_NAME,
47  DEFAULT_OFFSET,
48  DEFAULT_WORKDAYS,
49  DOMAIN,
50  LOGGER,
51 )
52 
53 
55  schema: vol.Schema,
56  country: str | None,
57 ) -> vol.Schema:
58  """Update schema with province from country."""
59  if not country:
60  return schema
61 
62  all_countries = list_supported_countries(include_aliases=False)
63 
64  language_schema = {}
65  province_schema = {}
66 
67  _country = country_holidays(country=country)
68  if country_default_language := (_country.default_language):
69  selectable_languages = _country.supported_languages
70  new_selectable_languages = list(selectable_languages)
71  language_schema = {
72  vol.Optional(
73  CONF_LANGUAGE, default=country_default_language
76  languages=new_selectable_languages, native_name=True
77  )
78  )
79  }
80 
81  if provinces := all_countries.get(country):
82  province_schema = {
83  vol.Optional(CONF_PROVINCE): SelectSelector(
85  options=provinces,
86  mode=SelectSelectorMode.DROPDOWN,
87  translation_key=CONF_PROVINCE,
88  )
89  ),
90  }
91 
92  category_schema = {}
93  # PUBLIC will always be included and can therefore not be set/removed
94  _categories = [x for x in _country.supported_categories if x != PUBLIC]
95  if _categories:
96  category_schema = {
97  vol.Optional(CONF_CATEGORY): SelectSelector(
99  options=_categories,
100  mode=SelectSelectorMode.DROPDOWN,
101  multiple=True,
102  translation_key=CONF_CATEGORY,
103  )
104  ),
105  }
106 
107  return vol.Schema(
108  {
109  **DATA_SCHEMA_OPT.schema,
110  **language_schema,
111  **province_schema,
112  **category_schema,
113  }
114  )
115 
116 
117 def _is_valid_date_range(check_date: str, error: type[HomeAssistantError]) -> bool:
118  """Validate date range."""
119  if check_date.find(",") > 0:
120  dates = check_date.split(",", maxsplit=1)
121  for date in dates:
122  if dt_util.parse_date(date) is None:
123  raise error("Incorrect date in range")
124  return True
125  return False
126 
127 
128 def validate_custom_dates(user_input: dict[str, Any]) -> None:
129  """Validate custom dates for add/remove holidays."""
130  for add_date in user_input[CONF_ADD_HOLIDAYS]:
131  if (
132  not _is_valid_date_range(add_date, AddDateRangeError)
133  and dt_util.parse_date(add_date) is None
134  ):
135  raise AddDatesError("Incorrect date")
136 
137  year: int = dt_util.now().year
138  if country := user_input.get(CONF_COUNTRY):
139  language = user_input.get(CONF_LANGUAGE)
140  province = user_input.get(CONF_PROVINCE)
141  obj_holidays = country_holidays(
142  country=country,
143  subdiv=province,
144  years=year,
145  language=language,
146  )
147  if (
148  supported_languages := obj_holidays.supported_languages
149  ) and language == "en":
150  for lang in supported_languages:
151  if lang.startswith("en"):
152  obj_holidays = country_holidays(
153  country,
154  subdiv=province,
155  years=year,
156  language=lang,
157  )
158  else:
159  obj_holidays = HolidayBase(years=year)
160 
161  for remove_date in user_input[CONF_REMOVE_HOLIDAYS]:
162  if (
163  not _is_valid_date_range(remove_date, RemoveDateRangeError)
164  and dt_util.parse_date(remove_date) is None
165  and obj_holidays.get_named(remove_date) == []
166  ):
167  raise RemoveDatesError("Incorrect date or name")
168 
169 
170 DATA_SCHEMA_OPT = vol.Schema(
171  {
172  vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): SelectSelector(
174  options=ALLOWED_DAYS,
175  multiple=True,
176  mode=SelectSelectorMode.DROPDOWN,
177  translation_key="days",
178  )
179  ),
180  vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): SelectSelector(
182  options=ALLOWED_DAYS,
183  multiple=True,
184  mode=SelectSelectorMode.DROPDOWN,
185  translation_key="days",
186  )
187  ),
188  vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): NumberSelector(
189  NumberSelectorConfig(min=-10, max=10, step=1, mode=NumberSelectorMode.BOX)
190  ),
191  vol.Optional(CONF_ADD_HOLIDAYS, default=[]): SelectSelector(
193  options=[],
194  multiple=True,
195  custom_value=True,
196  mode=SelectSelectorMode.DROPDOWN,
197  )
198  ),
199  vol.Optional(CONF_REMOVE_HOLIDAYS, default=[]): SelectSelector(
201  options=[],
202  multiple=True,
203  custom_value=True,
204  mode=SelectSelectorMode.DROPDOWN,
205  )
206  ),
207  }
208 )
209 
210 
211 class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
212  """Handle a config flow for Workday integration."""
213 
214  VERSION = 1
215 
216  data: dict[str, Any] = {}
217 
218  @staticmethod
219  @callback
221  config_entry: ConfigEntry,
222  ) -> WorkdayOptionsFlowHandler:
223  """Get the options flow for this handler."""
225 
226  async def async_step_user(
227  self, user_input: dict[str, Any] | None = None
228  ) -> ConfigFlowResult:
229  """Handle the user initial step."""
230  errors: dict[str, str] = {}
231 
232  supported_countries = await self.hass.async_add_executor_job(
233  partial(list_supported_countries, include_aliases=False)
234  )
235 
236  if user_input is not None:
237  self.datadata = user_input
238  return await self.async_step_optionsasync_step_options()
239  return self.async_show_formasync_show_formasync_show_form(
240  step_id="user",
241  data_schema=vol.Schema(
242  {
243  vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
244  vol.Optional(CONF_COUNTRY): CountrySelector(
246  countries=list(supported_countries),
247  )
248  ),
249  }
250  ),
251  errors=errors,
252  )
253 
255  self, user_input: dict[str, Any] | None = None
256  ) -> ConfigFlowResult:
257  """Handle remaining flow."""
258  errors: dict[str, str] = {}
259  if user_input is not None:
260  combined_input: dict[str, Any] = {**self.datadata, **user_input}
261 
262  try:
263  await self.hass.async_add_executor_job(
264  validate_custom_dates, combined_input
265  )
266  except AddDatesError:
267  errors["add_holidays"] = "add_holiday_error"
268  except AddDateRangeError:
269  errors["add_holidays"] = "add_holiday_range_error"
270  except RemoveDatesError:
271  errors["remove_holidays"] = "remove_holiday_error"
272  except RemoveDateRangeError:
273  errors["remove_holidays"] = "remove_holiday_range_error"
274 
275  abort_match = {
276  CONF_COUNTRY: combined_input.get(CONF_COUNTRY),
277  CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
278  CONF_OFFSET: combined_input[CONF_OFFSET],
279  CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
280  CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
281  CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
282  CONF_PROVINCE: combined_input.get(CONF_PROVINCE),
283  }
284  if CONF_CATEGORY in combined_input:
285  abort_match[CONF_CATEGORY] = combined_input[CONF_CATEGORY]
286  LOGGER.debug("abort_check in options with %s", combined_input)
287  self._async_abort_entries_match_async_abort_entries_match(abort_match)
288 
289  LOGGER.debug("Errors have occurred %s", errors)
290  if not errors:
291  LOGGER.debug("No duplicate, no errors, creating entry")
292  return self.async_create_entryasync_create_entryasync_create_entry(
293  title=combined_input[CONF_NAME],
294  data={},
295  options=combined_input,
296  )
297 
298  schema = await self.hass.async_add_executor_job(
299  add_province_and_language_to_schema,
300  DATA_SCHEMA_OPT,
301  self.datadata.get(CONF_COUNTRY),
302  )
303  new_schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(schema, user_input)
304  return self.async_show_formasync_show_formasync_show_form(
305  step_id="options",
306  data_schema=new_schema,
307  errors=errors,
308  description_placeholders={
309  "name": self.datadata[CONF_NAME],
310  "country": self.datadata.get(CONF_COUNTRY, "-"),
311  },
312  )
313 
314 
316  """Handle Workday options."""
317 
318  async def async_step_init(
319  self, user_input: dict[str, Any] | None = None
320  ) -> ConfigFlowResult:
321  """Manage Workday options."""
322  errors: dict[str, str] = {}
323 
324  if user_input is not None:
325  combined_input: dict[str, Any] = {**self.config_entryconfig_entryconfig_entry.options, **user_input}
326  if CONF_PROVINCE not in user_input:
327  # Province not present, delete old value (if present) too
328  combined_input.pop(CONF_PROVINCE, None)
329 
330  try:
331  await self.hass.async_add_executor_job(
332  validate_custom_dates, combined_input
333  )
334  except AddDatesError:
335  errors["add_holidays"] = "add_holiday_error"
336  except AddDateRangeError:
337  errors["add_holidays"] = "add_holiday_range_error"
338  except RemoveDatesError:
339  errors["remove_holidays"] = "remove_holiday_error"
340  except RemoveDateRangeError:
341  errors["remove_holidays"] = "remove_holiday_range_error"
342  else:
343  LOGGER.debug("abort_check in options with %s", combined_input)
344  abort_match = {
345  CONF_COUNTRY: self.config_entryconfig_entryconfig_entry.options.get(CONF_COUNTRY),
346  CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
347  CONF_OFFSET: combined_input[CONF_OFFSET],
348  CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
349  CONF_ADD_HOLIDAYS: combined_input[CONF_ADD_HOLIDAYS],
350  CONF_REMOVE_HOLIDAYS: combined_input[CONF_REMOVE_HOLIDAYS],
351  CONF_PROVINCE: combined_input.get(CONF_PROVINCE),
352  }
353  if CONF_CATEGORY in combined_input:
354  abort_match[CONF_CATEGORY] = combined_input[CONF_CATEGORY]
355  try:
356  self._async_abort_entries_match_async_abort_entries_match(abort_match)
357  except AbortFlow as err:
358  errors = {"base": err.reason}
359  else:
360  return self.async_create_entryasync_create_entry(data=combined_input)
361 
362  options = self.config_entryconfig_entryconfig_entry.options
363  schema: vol.Schema = await self.hass.async_add_executor_job(
364  add_province_and_language_to_schema,
365  DATA_SCHEMA_OPT,
366  options.get(CONF_COUNTRY),
367  )
368 
369  new_schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(schema, user_input or options)
370  LOGGER.debug("Errors have occurred in options %s", errors)
371  return self.async_show_formasync_show_form(
372  step_id="init",
373  data_schema=new_schema,
374  errors=errors,
375  description_placeholders={
376  "name": options[CONF_NAME],
377  "country": options.get(CONF_COUNTRY, "-"),
378  },
379  )
380 
381 
383  """Exception for error adding dates."""
384 
385 
386 class AddDateRangeError(HomeAssistantError):
387  """Exception for error adding dates."""
388 
389 
391  """Exception for error removing dates."""
392 
393 
395  """Exception for error removing dates."""
396 
397 
399  """Exception country does not exist error."""
WorkdayOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:222
ConfigFlowResult async_step_options(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:256
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:228
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:320
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 _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
None config_entry(self, ConfigEntry value)
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
_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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool _is_valid_date_range(str check_date, type[HomeAssistantError] error)
Definition: config_flow.py:117
None validate_custom_dates(dict[str, Any] user_input)
Definition: config_flow.py:128
vol.Schema add_province_and_language_to_schema(vol.Schema schema, str|None country)
Definition: config_flow.py:57