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:
5ila5
2024-07-25 21:16:08 +02:00
committed by 5ila5
parent bfeea0c435
commit 4d1aa92b21
6 changed files with 23078 additions and 408 deletions

View File

@@ -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,

View File

@@ -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": [

View File

@@ -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