mirror of
https://github.com/sascha-hemi/hacs_waste_collection_schedule.git
synced 2026-03-21 04:06:03 +01:00
config_flow add seperate step for each source
this way we can have individual translations per input filed and we can have individual descriptions for the whole form or input-field or on a input field basis, this way we can _move_ more documentation directly in the UI config_flow fix generics missing
This commit is contained in:
@@ -302,6 +302,22 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._sources = self._get_source_list()
|
self._sources = self._get_source_list()
|
||||||
self._options: dict = {}
|
self._options: dict = {}
|
||||||
|
for _, sources in self._sources.items():
|
||||||
|
for source in sources:
|
||||||
|
|
||||||
|
async def args_method(args_input):
|
||||||
|
return await self.async_step_args(args_input)
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
f"async_step_args_{source['module']}",
|
||||||
|
args_method,
|
||||||
|
)
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
f"async_step_reconfigure_{source['module']}",
|
||||||
|
args_method,
|
||||||
|
)
|
||||||
|
|
||||||
# Get source list from JSON
|
# Get source list from JSON
|
||||||
def _get_source_list(self) -> dict[str, list[SourceDict]]:
|
def _get_source_list(self) -> dict[str, list[SourceDict]]:
|
||||||
@@ -485,29 +501,28 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
for arg in args:
|
for arg in args:
|
||||||
default = args[arg].default
|
default = args[arg].default
|
||||||
arg_name = args[arg].name
|
arg_name = args[arg].name
|
||||||
arg_key = f"{source}_{arg_name}"
|
|
||||||
field_type = None
|
field_type = None
|
||||||
|
|
||||||
annotation = args[arg].annotation
|
annotation = args[arg].annotation
|
||||||
description = None
|
description = None
|
||||||
if args_input is not None and arg_key in args_input:
|
if args_input is not None and arg_name in args_input:
|
||||||
description = {"suggested_value": args_input[arg_key]}
|
description = {"suggested_value": args_input[arg_name]}
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
f"Setting suggested value for {arg_key} to {args_input[arg_key]} (previously filled in)"
|
f"Setting suggested value for {arg_name} to {args_input[arg_name]} (previously filled in)"
|
||||||
)
|
)
|
||||||
elif arg_key in pre_filled:
|
elif arg_name in pre_filled:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
f"Setting default value for {arg_key} to {pre_filled[arg_key]}"
|
f"Setting default value for {arg_name} to {pre_filled[arg_name]}"
|
||||||
)
|
)
|
||||||
description = {
|
description = {
|
||||||
"suggested_value": pre_filled[arg_key],
|
"suggested_value": pre_filled[arg_name],
|
||||||
}
|
}
|
||||||
if annotation != inspect._empty:
|
if annotation != inspect._empty:
|
||||||
field_type = (
|
field_type = (
|
||||||
await self.__get_type_by_annotation(annotation) or field_type
|
await self.__get_type_by_annotation(annotation) or field_type
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
f"Default for {arg_key}: {type(default) if default is not inspect.Signature.empty else inspect.Signature.empty}"
|
f"Default for {arg_name}: {type(default) if default is not inspect.Signature.empty else inspect.Signature.empty}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if arg_name in MODULE_FLOW_TYPES:
|
if arg_name in MODULE_FLOW_TYPES:
|
||||||
@@ -532,14 +547,14 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
arg
|
arg
|
||||||
].name in suggestions:
|
].name in suggestions:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
f"Adding suggestions to {arg_key}: {suggestions[arg_key]}"
|
f"Adding suggestions to {arg_name}: {suggestions[arg_name]}"
|
||||||
)
|
)
|
||||||
# Add suggestions to the field if fetch/init raised an Exception with suggestions
|
# Add suggestions to the field if fetch/init raised an Exception with suggestions
|
||||||
field_type = SelectSelector(
|
field_type = SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=[
|
options=[
|
||||||
SelectOptionDict(label=x, value=x)
|
SelectOptionDict(label=x, value=x)
|
||||||
for x in suggestions[arg_key]
|
for x in suggestions[arg_name]
|
||||||
],
|
],
|
||||||
mode=SelectSelectorMode.DROPDOWN,
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
custom_value=True,
|
custom_value=True,
|
||||||
@@ -548,7 +563,7 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
)
|
)
|
||||||
|
|
||||||
if default == inspect.Signature.empty:
|
if default == inspect.Signature.empty:
|
||||||
vol_args[vol.Required(arg_key, description=description)] = (
|
vol_args[vol.Required(arg_name, description=description)] = (
|
||||||
field_type or str
|
field_type or str
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -556,7 +571,7 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
# Handle boolean, int, string, date, datetime, list defaults
|
# Handle boolean, int, string, date, datetime, list defaults
|
||||||
vol_args[
|
vol_args[
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
arg_key,
|
arg_name,
|
||||||
default=UNDEFINED if default is None else default,
|
default=UNDEFINED if default is None else default,
|
||||||
description=description,
|
description=description,
|
||||||
)
|
)
|
||||||
@@ -565,7 +580,7 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
f"Unsupported type: {type(default)}: {arg_key}: {default}: {field_type}"
|
f"Unsupported type: {type(default)}: {arg_name}: {default}: {field_type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
schema = vol.Schema(vol_args)
|
schema = vol.Schema(vol_args)
|
||||||
@@ -586,7 +601,6 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
"""
|
"""
|
||||||
errors = {}
|
errors = {}
|
||||||
description_placeholders: dict[str, str] = {}
|
description_placeholders: dict[str, str] = {}
|
||||||
args_input = {k.removeprefix(f"{source}_"): v for k, v in args_input.items()}
|
|
||||||
|
|
||||||
if hasattr(module, "validate_params"):
|
if hasattr(module, "validate_params"):
|
||||||
errors.update(module.validate_params(args_input))
|
errors.update(module.validate_params(args_input))
|
||||||
@@ -613,17 +627,16 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
except SourceArgumentSuggestionsExceptionBase as e:
|
except SourceArgumentSuggestionsExceptionBase as e:
|
||||||
if not hasattr(self, "_error_suggestions"):
|
if not hasattr(self, "_error_suggestions"):
|
||||||
self._error_suggestions = {}
|
self._error_suggestions = {}
|
||||||
arg_key = f"{source}_{e.argument}"
|
self._error_suggestions.update({e.argument: e.suggestions})
|
||||||
self._error_suggestions.update({arg_key: e.suggestions})
|
errors[e.argument] = "invalid_arg"
|
||||||
errors[arg_key] = "invalid_arg"
|
|
||||||
description_placeholders["invalid_arg_message"] = e.simple_message
|
description_placeholders["invalid_arg_message"] = e.simple_message
|
||||||
if e.suggestion_type != str and e.suggestion_type != int:
|
if e.suggestion_type != str and e.suggestion_type != int:
|
||||||
description_placeholders["invalid_arg_message"] = e.message
|
description_placeholders["invalid_arg_message"] = e.message
|
||||||
except SourceArgumentRequired as e:
|
except SourceArgumentRequired as e:
|
||||||
errors[f"{source}_{e.argument}"] = "invalid_arg"
|
errors[e.argument] = "invalid_arg"
|
||||||
description_placeholders["invalid_arg_message"] = e.message
|
description_placeholders["invalid_arg_message"] = e.message
|
||||||
except SourceArgumentException as e:
|
except SourceArgumentException as e:
|
||||||
errors[f"{source}_{e.argument}"] = "invalid_arg"
|
errors[e.argument] = "invalid_arg"
|
||||||
description_placeholders["invalid_arg_message"] = e.message
|
description_placeholders["invalid_arg_message"] = e.message
|
||||||
except SourceArgumentExceptionMultiple as e:
|
except SourceArgumentExceptionMultiple as e:
|
||||||
description_placeholders["invalid_arg_message"] = e.message
|
description_placeholders["invalid_arg_message"] = e.message
|
||||||
@@ -637,6 +650,17 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
description_placeholders["fetch_error_message"] = str(e)
|
description_placeholders["fetch_error_message"] = str(e)
|
||||||
return errors, description_placeholders, options
|
return errors, description_placeholders, options
|
||||||
|
|
||||||
|
async def async_source_selected(self) -> None:
|
||||||
|
async def args_method(args_input):
|
||||||
|
return await self.async_step_args(args_input)
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
f"async_step_args_{self._source}",
|
||||||
|
args_method,
|
||||||
|
)
|
||||||
|
return await self.async_step_args()
|
||||||
|
|
||||||
# Step 3: User fills in source arguments
|
# Step 3: User fills in source arguments
|
||||||
async def async_step_args(self, args_input=None) -> ConfigFlowResult:
|
async def async_step_args(self, args_input=None) -> ConfigFlowResult:
|
||||||
self._source = cast(str, self._source)
|
self._source = cast(str, self._source)
|
||||||
@@ -668,7 +692,7 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
self.async_show_form(step_id="options")
|
self.async_show_form(step_id="options")
|
||||||
return await self.async_step_flow_type()
|
return await self.async_step_flow_type()
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="args",
|
step_id=f"args_{self._source}",
|
||||||
data_schema=schema,
|
data_schema=schema,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
@@ -832,7 +856,7 @@ class WasteCollectionConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call
|
|||||||
reason="reconfigure_successful",
|
reason="reconfigure_successful",
|
||||||
)
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reconfigure",
|
step_id=f"reconfigure_{source}",
|
||||||
data_schema=schema,
|
data_schema=schema,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
|
|||||||
@@ -3227,12 +3227,14 @@
|
|||||||
{
|
{
|
||||||
"title": "ICS",
|
"title": "ICS",
|
||||||
"module": "ics",
|
"module": "ics",
|
||||||
"default_params": {}
|
"default_params": {},
|
||||||
|
"id": "ics"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Static Source",
|
"title": "Static Source",
|
||||||
"module": "static",
|
"module": "static",
|
||||||
"default_params": {}
|
"default_params": {},
|
||||||
|
"id": "static"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Germany": [
|
"Germany": [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@ END_SERVICE_SECTION = "<!--End of service section-->"
|
|||||||
|
|
||||||
LANGUAGES = ["en", "de"]
|
LANGUAGES = ["en", "de"]
|
||||||
ARG_TRANSLATIONS_TO_KEEP = ["calendar_title"]
|
ARG_TRANSLATIONS_TO_KEEP = ["calendar_title"]
|
||||||
|
ARG_DESCRIPTIONS_TO_KEEP = ["calendar_title"]
|
||||||
|
ARG_GENERAL_KEYS_TO_KEEP = ["title", "description"]
|
||||||
|
|
||||||
|
|
||||||
class SourceInfo:
|
class SourceInfo:
|
||||||
@@ -206,6 +208,8 @@ def browse_sources() -> list[SourceInfo]:
|
|||||||
|
|
||||||
sig = inspect.signature(module.Source.__init__)
|
sig = inspect.signature(module.Source.__init__)
|
||||||
params = [param.name for param in sig.parameters.values()]
|
params = [param.name for param in sig.parameters.values()]
|
||||||
|
if "self" in params:
|
||||||
|
params.remove("self")
|
||||||
param_translations = getattr(module, "PARAM_TRANSLATIONS", {})
|
param_translations = getattr(module, "PARAM_TRANSLATIONS", {})
|
||||||
|
|
||||||
filename = f"/doc/source/{f}.md"
|
filename = f"/doc/source/{f}.md"
|
||||||
@@ -369,18 +373,10 @@ def beautify_url(url):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def update_json(
|
def update_sources_json(countries: dict[str, list[SourceInfo]]) -> None:
|
||||||
countries: dict[str, list[SourceInfo]], generics: list[SourceInfo] = []
|
|
||||||
):
|
|
||||||
params = set()
|
|
||||||
param_translations: dict[str, dict[str, str]] = {}
|
|
||||||
countries = countries.copy()
|
|
||||||
countries["Generic"] = generics
|
|
||||||
# generate country list
|
|
||||||
output: dict[str, list[dict[str, str | dict[str, Any]]]] = {}
|
output: dict[str, list[dict[str, str | dict[str, Any]]]] = {}
|
||||||
for country in sorted(countries):
|
for country in sorted(countries):
|
||||||
output[country] = []
|
output[country] = []
|
||||||
|
|
||||||
for e in sorted(
|
for e in sorted(
|
||||||
countries[country],
|
countries[country],
|
||||||
key=lambda e: (e.title.lower(), beautify_url(e.url), e.filename),
|
key=lambda e: (e.title.lower(), beautify_url(e.url), e.filename),
|
||||||
@@ -402,17 +398,55 @@ def update_json(
|
|||||||
"default_params": e.extra_info_default_params,
|
"default_params": e.extra_info_default_params,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
params.update([f"{e.module}_{p}" for p in e.params])
|
with open(
|
||||||
|
"custom_components/waste_collection_schedule/sources.json",
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
f.write(json.dumps(output, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def get_custom_translations(
|
||||||
|
countries: dict[str, list[SourceInfo]]
|
||||||
|
) -> dict[str, dict[str, dict[str, str | None]]]:
|
||||||
|
"""gets all parameters and its custom translations for all languages
|
||||||
|
|
||||||
|
Args:
|
||||||
|
countries (dict[str, list[SourceInfo]]):
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, dict[str, dict[str, str|None]]]: dict[MODULE][PARAM][LANG][TRANSLATION|None]
|
||||||
|
"""
|
||||||
|
param_translations: dict[str, dict[str, dict[str, str | None]]] = {}
|
||||||
|
for country in sorted(countries):
|
||||||
|
for e in sorted(
|
||||||
|
countries[country],
|
||||||
|
key=lambda e: (e.title.lower(), beautify_url(e.url), e.filename),
|
||||||
|
):
|
||||||
|
if e.module is None: # ICS source
|
||||||
|
continue
|
||||||
|
if not e.module in param_translations:
|
||||||
|
param_translations[e.module] = {}
|
||||||
|
|
||||||
|
for param in e.params:
|
||||||
|
if param not in param_translations[e.module]:
|
||||||
|
param_translations[e.module][param] = {}
|
||||||
|
|
||||||
for lang, translations in e.custom_param_translation.items():
|
for lang, translations in e.custom_param_translation.items():
|
||||||
if lang in param_translations:
|
for param, translation in translations.items():
|
||||||
param_translations[lang].update(
|
param_translations[e.module][param][lang] = translation
|
||||||
{f"{e.module}_{k}": v for k, v in translations.items()}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
param_translations[lang] = translations.copy()
|
|
||||||
|
|
||||||
# output["Generic"] = [{"title": "ICS", "module": "ics", "default_params": {}}, {"title": "Static", "module": "static", "default_params": {}}]
|
return param_translations
|
||||||
|
|
||||||
|
|
||||||
|
def update_json(
|
||||||
|
countries: dict[str, list[SourceInfo]], generics: list[SourceInfo] = []
|
||||||
|
):
|
||||||
|
countries = countries.copy()
|
||||||
|
countries["Generic"] = generics
|
||||||
|
update_sources_json(countries)
|
||||||
|
|
||||||
|
param_translations = get_custom_translations(countries)
|
||||||
for lang in LANGUAGES:
|
for lang in LANGUAGES:
|
||||||
tranlation_file = (
|
tranlation_file = (
|
||||||
f"custom_components/waste_collection_schedule/translations/{lang}.json"
|
f"custom_components/waste_collection_schedule/translations/{lang}.json"
|
||||||
@@ -427,24 +461,68 @@ def update_json(
|
|||||||
) as f:
|
) as f:
|
||||||
translations = json.load(f)
|
translations = json.load(f)
|
||||||
|
|
||||||
arg_translations = {}
|
translation_for_all = {}
|
||||||
|
description_for_all_args = {}
|
||||||
|
description_for_all_reconfigure = {}
|
||||||
|
|
||||||
|
keys_for_all_args = {}
|
||||||
|
keys_for_all_reconfigure = {}
|
||||||
|
|
||||||
for key, value in translations["config"]["step"]["args"]["data"].items():
|
for key, value in translations["config"]["step"]["args"]["data"].items():
|
||||||
if key in ARG_TRANSLATIONS_TO_KEEP:
|
if key in ARG_TRANSLATIONS_TO_KEEP:
|
||||||
arg_translations[key] = value
|
translation_for_all[key] = value
|
||||||
|
|
||||||
for param in params:
|
for key, value in (
|
||||||
param = f"{param}"
|
translations["config"]["step"]["args"].get("data_description", {}).items()
|
||||||
if param in param_translations.get(lang, {}):
|
):
|
||||||
arg_translations[param] = param_translations[lang][param]
|
if key in ARG_DESCRIPTIONS_TO_KEEP:
|
||||||
elif lang == "en" and param not in arg_translations:
|
description_for_all_args[key] = value
|
||||||
arg_translations[param] = " ".join(
|
for key, value in (
|
||||||
[s.capitalize() for s in split_camel_and_snake_case(param)]
|
translations["config"]["step"]["reconfigure"]
|
||||||
)
|
.get("data_description", {})
|
||||||
|
.items()
|
||||||
|
):
|
||||||
|
if key in ARG_DESCRIPTIONS_TO_KEEP:
|
||||||
|
description_for_all_reconfigure[key] = value
|
||||||
|
|
||||||
arg_translations = {k: arg_translations[k] for k in sorted(arg_translations)}
|
for key, value in translations["config"]["step"]["args"].items():
|
||||||
|
if key in ARG_GENERAL_KEYS_TO_KEEP:
|
||||||
|
keys_for_all_args[key] = value
|
||||||
|
for key, value in translations["config"]["step"]["reconfigure"].items():
|
||||||
|
if key in ARG_GENERAL_KEYS_TO_KEEP:
|
||||||
|
keys_for_all_reconfigure[key] = value
|
||||||
|
|
||||||
translations["config"]["step"]["args"]["data"] = arg_translations
|
for module, module_params in param_translations.items():
|
||||||
translations["config"]["step"]["reconfigure"]["data"] = arg_translations
|
translations["config"]["step"][f"args_{module}"] = keys_for_all_args.copy()
|
||||||
|
translations["config"]["step"][
|
||||||
|
f"reconfigure_{module}"
|
||||||
|
] = keys_for_all_reconfigure.copy()
|
||||||
|
|
||||||
|
translations["config"]["step"][f"args_{module}"][
|
||||||
|
"data"
|
||||||
|
] = translation_for_all.copy()
|
||||||
|
translations["config"]["step"][f"reconfigure_{module}"][
|
||||||
|
"data"
|
||||||
|
] = translation_for_all.copy()
|
||||||
|
|
||||||
|
translations["config"]["step"][f"args_{module}"][
|
||||||
|
"data_description"
|
||||||
|
] = description_for_all_args.copy()
|
||||||
|
translations["config"]["step"][f"reconfigure_{module}"][
|
||||||
|
"data_description"
|
||||||
|
] = description_for_all_reconfigure.copy()
|
||||||
|
|
||||||
|
for param, languages in module_params.items():
|
||||||
|
if languages.get(lang, None) is None:
|
||||||
|
languages[lang] = " ".join(
|
||||||
|
[s.capitalize() for s in split_camel_and_snake_case(param)]
|
||||||
|
)
|
||||||
|
translations["config"]["step"][f"args_{module}"]["data"][
|
||||||
|
param
|
||||||
|
] = languages[lang]
|
||||||
|
translations["config"]["step"][f"reconfigure_{module}"]["data"][
|
||||||
|
param
|
||||||
|
] = languages[lang]
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
tranlation_file,
|
tranlation_file,
|
||||||
@@ -453,13 +531,6 @@ def update_json(
|
|||||||
) as f:
|
) as f:
|
||||||
json.dump(translations, f, indent=2, ensure_ascii=False)
|
json.dump(translations, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
with open(
|
|
||||||
"custom_components/waste_collection_schedule/sources.json",
|
|
||||||
"w",
|
|
||||||
encoding="utf-8",
|
|
||||||
) as f:
|
|
||||||
f.write(json.dumps(output, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
def update_readme_md(countries: dict[str, list[SourceInfo]]):
|
def update_readme_md(countries: dict[str, list[SourceInfo]]):
|
||||||
# generate country list
|
# generate country list
|
||||||
|
|||||||
Reference in New Issue
Block a user