From 139489822dab95807da1284ec4c1b3c3bb39b9a8 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 20 Dec 2022 22:47:20 +0100 Subject: [PATCH] Review Configuration options for TV lists (#206) * Add Sources, Apps and Channels lists validation in options forms * Reorganize options menu * Manage errors in parsing applications list (issue #204) --- README.md | 12 +- .../samsungtv_smart/config_flow.py | 192 +++++++++++------- .../samsungtv_smart/media_player.py | 38 ++-- .../samsungtv_smart/translations/en.json | 10 +- .../samsungtv_smart/translations/it.json | 10 +- 5 files changed, 160 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 4cb7399..bb72ca6 100644 --- a/README.md +++ b/README.md @@ -253,9 +253,9 @@ generation not work with some TV models.
Example value: ``` - 1| Netflix: 11101200001 - 2| YouTube: 111299001912 - 3| Spotify: 3201606009684 + 1| Netflix: "11101200001" + 2| YouTube: "111299001912" + 3| Spotify: "3201606009684" ``` Known lists of App IDs: [List 1](https://github.com/tavicu/homebridge-samsung-tizen/issues/26#issuecomment-447424879), @@ -270,9 +270,9 @@ You can configure the pair list `Name: Key` using the yaml editor in the option Example value: ``` - 1| MTV: 14 - 2| Eurosport: 20 - 3| TLC: 21 + 1| MTV: "14" + 2| Eurosport: "20" + 3| TLC: "21" ``` You can also specify the source that must be used for every channel. The source must be one of the source name defined in the `source_list`
diff --git a/custom_components/samsungtv_smart/config_flow.py b/custom_components/samsungtv_smart/config_flow.py index 87aac93..af8f043 100644 --- a/custom_components/samsungtv_smart/config_flow.py +++ b/custom_components/samsungtv_smart/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from numbers import Number import socket from typing import Any, Dict @@ -542,15 +543,112 @@ async def async_step_menu(self, _=None): return self.async_show_menu( step_id="menu", menu_options=[ - "init", - "adv_opt", - "sync_ent", "source_list", "app_list", "channel_list", + "sync_ent", + "adv_opt", + "init", + "save_exit", ], ) + async def async_step_save_exit(self, _): + """Handle save and exit flow.""" + return self._save_entry(data=self._std_options) + + async def async_step_source_list(self, user_input=None): + """Handle sources list flow.""" + errors: dict[str, str] | None = None + if user_input is not None: + valid_list = _validate_tv_list(user_input[CONF_SOURCE_LIST]) + if valid_list is not None: + self._source_list = valid_list + return await self.async_step_menu() + errors = {CONF_BASE: "invalid_tv_list"} + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SOURCE_LIST, default=self._source_list + ): ObjectSelector() + } + ) + return self.async_show_form( + step_id="source_list", data_schema=data_schema, errors=errors + ) + + async def async_step_app_list(self, user_input=None): + """Handle apps list flow.""" + errors: dict[str, str] | None = None + if user_input is not None: + valid_list = _validate_tv_list(user_input[CONF_APP_LIST]) + if valid_list is not None: + self._app_list = valid_list + return await self.async_step_menu() + errors = {CONF_BASE: "invalid_tv_list"} + + data_schema = vol.Schema( + {vol.Optional(CONF_APP_LIST, default=self._app_list): ObjectSelector()} + ) + return self.async_show_form( + step_id="app_list", data_schema=data_schema, errors=errors + ) + + async def async_step_channel_list(self, user_input=None): + """Handle channels list flow.""" + errors: dict[str, str] | None = None + if user_input is not None: + valid_list = _validate_tv_list(user_input[CONF_CHANNEL_LIST]) + if valid_list is not None: + self._channel_list = valid_list + return await self.async_step_menu() + errors = {CONF_BASE: "invalid_tv_list"} + + data_schema = vol.Schema( + { + vol.Optional( + CONF_CHANNEL_LIST, default=self._channel_list + ): ObjectSelector() + } + ) + return self.async_show_form( + step_id="channel_list", data_schema=data_schema, errors=errors + ) + + async def async_step_sync_ent(self, user_input=None): + """Handle syncronized entity flow.""" + if user_input is not None: + self._sync_ent_opt = user_input + return await self.async_step_menu() + return self._async_sync_ent_form() + + @callback + def _async_sync_ent_form(self): + """Return configuration form for syncronized entity.""" + select_entities = EntitySelectorConfig( + domain=_async_get_domains_service(self.hass, SERVICE_TURN_ON), + exclude_entities=_async_get_entry_entities(self.hass, self._entry_id), + multiple=True, + ) + options = _validate_options(self._sync_ent_opt) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SYNC_TURN_OFF, + description={ + "suggested_value": options.get(CONF_SYNC_TURN_OFF, []) + }, + ): EntitySelector(select_entities), + vol.Optional( + CONF_SYNC_TURN_ON, + description={"suggested_value": options.get(CONF_SYNC_TURN_ON, [])}, + ): EntitySelector(select_entities), + } + ) + return self.async_show_form(step_id="sync_ent", data_schema=data_schema) + async def async_step_adv_opt(self, user_input=None): """Handle advanced options flow.""" if user_input is not None: @@ -605,80 +703,6 @@ def _async_adv_opt_form(self): ) return self.async_show_form(step_id="adv_opt", data_schema=data_schema) - async def async_step_sync_ent(self, user_input=None): - """Handle syncronized entity flow.""" - if user_input is not None: - self._sync_ent_opt = user_input - return await self.async_step_menu() - return self._async_sync_ent_form() - - @callback - def _async_sync_ent_form(self): - """Return configuration form for syncronized entity.""" - select_entities = EntitySelectorConfig( - domain=_async_get_domains_service(self.hass, SERVICE_TURN_ON), - exclude_entities=_async_get_entry_entities(self.hass, self._entry_id), - multiple=True, - ) - options = _validate_options(self._sync_ent_opt) - - data_schema = vol.Schema( - { - vol.Optional( - CONF_SYNC_TURN_OFF, - description={ - "suggested_value": options.get(CONF_SYNC_TURN_OFF, []) - }, - ): EntitySelector(select_entities), - vol.Optional( - CONF_SYNC_TURN_ON, - description={"suggested_value": options.get(CONF_SYNC_TURN_ON, [])}, - ): EntitySelector(select_entities), - } - ) - return self.async_show_form(step_id="sync_ent", data_schema=data_schema) - - async def async_step_app_list(self, user_input=None): - """Handle apps list flow.""" - if user_input is not None: - self._app_list = user_input[CONF_APP_LIST] - return await self.async_step_menu() - - data_schema = vol.Schema( - {vol.Optional(CONF_APP_LIST, default=self._app_list): ObjectSelector()} - ) - return self.async_show_form(step_id="app_list", data_schema=data_schema) - - async def async_step_channel_list(self, user_input=None): - """Handle channels list flow.""" - if user_input is not None: - self._channel_list = user_input[CONF_CHANNEL_LIST] - return await self.async_step_menu() - - data_schema = vol.Schema( - { - vol.Optional( - CONF_CHANNEL_LIST, default=self._channel_list - ): ObjectSelector() - } - ) - return self.async_show_form(step_id="channel_list", data_schema=data_schema) - - async def async_step_source_list(self, user_input=None): - """Handle sources list flow.""" - if user_input is not None: - self._source_list = user_input[CONF_SOURCE_LIST] - return await self.async_step_menu() - - data_schema = vol.Schema( - { - vol.Optional( - CONF_SOURCE_LIST, default=self._source_list - ): ObjectSelector() - } - ) - return self.async_show_form(step_id="source_list", data_schema=data_schema) - def _validate_options(options: dict): """Validate options format""" @@ -694,6 +718,20 @@ def _validate_options(options: dict): return valid_options +def _validate_tv_list(input_list: dict[str, Any]) -> dict[str, str] | None: + """Validate TV list from object selector.""" + valid_list = {} + for name_val, id_val in input_list.items(): + if not id_val: + continue + if isinstance(id_val, Number): + id_val = str(id_val) + if not isinstance(id_val, str): + return None + valid_list[name_val] = id_val + return valid_list + + def _dict_to_select(opt_dict: dict): """Covert a dict to a SelectSelectorConfig.""" return SelectSelectorConfig( diff --git a/custom_components/samsungtv_smart/media_player.py b/custom_components/samsungtv_smart/media_player.py index 11ef102..242fb37 100644 --- a/custom_components/samsungtv_smart/media_player.py +++ b/custom_components/samsungtv_smart/media_player.py @@ -400,21 +400,33 @@ def _get_add_dev_info(dev_model, dev_name, dev_os, dev_mac): return dict(dev_info) @staticmethod - def _split_app_list(app_list, sep=ST_APP_SEPARATOR): + def _split_app_list(app_list: dict[str, str]) -> list[dict[str, str]]: """Split the application list for standard and SmartThings.""" - retval = {"app": {}, "appST": {}} + apps = {} + apps_st = {} - for app_name, value in app_list.items(): - value_split = value.split(sep, 1) - app_id = value_split[0] - if len(value_split) == 1: + for app_name, app_ids in app_list.items(): + try: + app_id_split = app_ids.split(ST_APP_SEPARATOR, 1) + except (ValueError, AttributeError): + _LOGGER.warning( + "Invalid ID [%s] for App [%s] will be ignored." + " Use integration options to correct the App ID", + app_ids, + app_name, + ) + continue + + app_id = app_id_split[0] + if len(app_id_split) == 1: _, st_app_id, _ = _get_default_app_info(app_id) else: - st_app_id = value_split[1] - retval["app"][app_name] = app_id - retval["appST"][app_name] = st_app_id or app_id + st_app_id = app_id_split[1] + + apps[app_name] = app_id + apps_st[app_name] = st_app_id or app_id - return retval + return [apps, apps_st] def _load_tv_lists(self, first_load=False): """Load TV sources, apps and channels.""" @@ -431,9 +443,9 @@ def _load_tv_lists(self, first_load=False): # load apps list app_list = self._get_option(CONF_APP_LIST, {}) if app_list: - double_list = self._split_app_list(app_list, "/") - self._app_list = double_list["app"] - self._app_list_st = double_list["appST"] + double_list = self._split_app_list(app_list) + self._app_list = double_list[0] + self._app_list_st = double_list[1] else: self._app_list = None if first_load else {} self._app_list_st = None if first_load else {} diff --git a/custom_components/samsungtv_smart/translations/en.json b/custom_components/samsungtv_smart/translations/en.json index 51f6ebd..585dfe4 100644 --- a/custom_components/samsungtv_smart/translations/en.json +++ b/custom_components/samsungtv_smart/translations/en.json @@ -60,12 +60,13 @@ "menu": { "title": "SamsungTV Smart options menu", "menu_options": { - "init": "Back to main page", "adv_opt": "Advanced options", - "sync_ent": "Synched entities configuration", "app_list": "Applications list configuration", "channel_list": "Channels list configuration", - "source_list": "Sources list configuration" + "init": "Back to basic options", + "save_exit": "Save options and exit", + "source_list": "Sources list configuration", + "sync_ent": "Synched entities configuration" } }, "adv_opt": { @@ -106,6 +107,9 @@ "source_list": "Sources list:" } } + }, + "error": { + "invalid_tv_list": "Invalid format. Please check documentation" } } } diff --git a/custom_components/samsungtv_smart/translations/it.json b/custom_components/samsungtv_smart/translations/it.json index 037966f..168e8b6 100644 --- a/custom_components/samsungtv_smart/translations/it.json +++ b/custom_components/samsungtv_smart/translations/it.json @@ -60,12 +60,13 @@ "menu": { "title": "Menù opzioni SamsungTV Smart", "menu_options": { - "init": "Torna alla pagina principale", "adv_opt": "Opzioni avanzate", - "sync_ent": "Configurazione entità collegate", "app_list": "Configurazione lista applicazioni", "channel_list": "Configurazione lista canali", - "source_list": "Configurazione lista sorgenti" + "init": "Torna alle opzioni di base", + "save_exit": "Salva le opzioni ed esci", + "source_list": "Configurazione lista sorgenti", + "sync_ent": "Configurazione entità collegate" } }, "adv_opt": { @@ -106,6 +107,9 @@ "source_list": "Lista sorgenti:" } } + }, + "error": { + "invalid_tv_list": "Formato not valido. Controlla la documentazione" } } }