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"
}
}
}