-
-
Notifications
You must be signed in to change notification settings - Fork 30k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add config flow * add tests * add user step error handling * remove unload function * add missing test file * handle authentication correctly * remove old discovery mode * better handling of remote class * optimized abort messages * add already configured test for user flow * Import order * use ip property instead context * Black * small syntax * use snake_case * Revert "use ip property instead context" This reverts commit 9150240. * disable wrong pylint errors * disable wrong no-member * Try to fix review comments * Try to fix review comments * Fix missing self * Fix ip checks * methods to functions * simplify user check * remove user errors * use async_setup for config * fix after rebase * import config to user config flow * patch all samsungctl * fix after rebase * fix notes * remove unused variable * ignore old setup function * fix after merge * pass configuration to import step * isort * fix recursion * remove timeout config * add turn on action (dry without testing) * use upstream checks * cleanup * minor * correctly await async method * ignore unused import * async call send_key * Revert "async call send_key" This reverts commit f370578. * fix comments * fix timeout test * test turn on action * Update media_player.py * Update test_media_player.py * Update test_media_player.py * use async executor * use newer ssdp data * update manually configured with ssdp data * dont setup component directly * ensure list * check updated device info * Update config_flow.py * Update __init__.py * fix duplicate check * simplified unique check * move method detection to config_flow * move unique test to init * fix after real world test * optimize config_validation * update device_info on ssdp discovery * cleaner update listener * fix lint * fix method signature * add note for manual config to confirm message * fix turn_on_action * pass script * patch delay * remove device info update
- Loading branch information
1 parent
4fb3645
commit ef05aa2
Showing
14 changed files
with
898 additions
and
399 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,60 @@ | ||
"""The Samsung TV integration.""" | ||
import socket | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT | ||
import homeassistant.helpers.config_validation as cv | ||
|
||
from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN | ||
|
||
|
||
def ensure_unique_hosts(value): | ||
"""Validate that all configs have a unique host.""" | ||
vol.Schema(vol.Unique("duplicate host entries found"))( | ||
[socket.gethostbyname(entry[CONF_HOST]) for entry in value] | ||
) | ||
return value | ||
|
||
|
||
CONFIG_SCHEMA = vol.Schema( | ||
{ | ||
DOMAIN: vol.All( | ||
cv.ensure_list, | ||
[ | ||
vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST): cv.string, | ||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Optional(CONF_PORT): cv.port, | ||
vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, | ||
} | ||
) | ||
], | ||
ensure_unique_hosts, | ||
) | ||
}, | ||
extra=vol.ALLOW_EXTRA, | ||
) | ||
|
||
|
||
async def async_setup(hass, config): | ||
"""Set up the Samsung TV integration.""" | ||
if DOMAIN in config: | ||
for entry_config in config[DOMAIN]: | ||
hass.async_create_task( | ||
hass.config_entries.flow.async_init( | ||
DOMAIN, context={"source": "import"}, data=entry_config | ||
) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_setup_entry(hass, entry): | ||
"""Set up the Samsung TV platform.""" | ||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(entry, "media_player") | ||
) | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
"""Config flow for Samsung TV.""" | ||
import socket | ||
from urllib.parse import urlparse | ||
|
||
from samsungctl import Remote | ||
from samsungctl.exceptions import AccessDenied, UnhandledResponse | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.ssdp import ( | ||
ATTR_SSDP_LOCATION, | ||
ATTR_UPNP_FRIENDLY_NAME, | ||
ATTR_UPNP_MANUFACTURER, | ||
ATTR_UPNP_MODEL_NAME, | ||
ATTR_UPNP_UDN, | ||
) | ||
from homeassistant.const import ( | ||
CONF_HOST, | ||
CONF_ID, | ||
CONF_IP_ADDRESS, | ||
CONF_METHOD, | ||
CONF_NAME, | ||
CONF_PORT, | ||
) | ||
|
||
# pylint:disable=unused-import | ||
from .const import ( | ||
CONF_MANUFACTURER, | ||
CONF_MODEL, | ||
CONF_ON_ACTION, | ||
DOMAIN, | ||
LOGGER, | ||
METHODS, | ||
) | ||
|
||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) | ||
|
||
RESULT_AUTH_MISSING = "auth_missing" | ||
RESULT_SUCCESS = "success" | ||
RESULT_NOT_FOUND = "not_found" | ||
RESULT_NOT_SUPPORTED = "not_supported" | ||
|
||
|
||
def _get_ip(host): | ||
if host is None: | ||
return None | ||
return socket.gethostbyname(host) | ||
|
||
|
||
class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a Samsung TV config flow.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL | ||
|
||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 | ||
|
||
def __init__(self): | ||
"""Initialize flow.""" | ||
self._host = None | ||
self._ip = None | ||
self._manufacturer = None | ||
self._method = None | ||
self._model = None | ||
self._name = None | ||
self._on_script = None | ||
self._port = None | ||
self._title = None | ||
self._uuid = None | ||
|
||
def _get_entry(self): | ||
return self.async_create_entry( | ||
title=self._title, | ||
data={ | ||
CONF_HOST: self._host, | ||
CONF_ID: self._uuid, | ||
CONF_IP_ADDRESS: self._ip, | ||
CONF_MANUFACTURER: self._manufacturer, | ||
CONF_METHOD: self._method, | ||
CONF_MODEL: self._model, | ||
CONF_NAME: self._name, | ||
CONF_ON_ACTION: self._on_script, | ||
CONF_PORT: self._port, | ||
}, | ||
) | ||
|
||
def _try_connect(self): | ||
"""Try to connect and check auth.""" | ||
for method in METHODS: | ||
config = { | ||
"name": "HomeAssistant", | ||
"description": "HomeAssistant", | ||
"id": "ha.component.samsung", | ||
"host": self._host, | ||
"method": method, | ||
"port": self._port, | ||
"timeout": 1, | ||
} | ||
try: | ||
LOGGER.debug("Try config: %s", config) | ||
with Remote(config.copy()): | ||
LOGGER.debug("Working config: %s", config) | ||
self._method = method | ||
return RESULT_SUCCESS | ||
except AccessDenied: | ||
LOGGER.debug("Working but denied config: %s", config) | ||
return RESULT_AUTH_MISSING | ||
except UnhandledResponse: | ||
LOGGER.debug("Working but unsupported config: %s", config) | ||
return RESULT_NOT_SUPPORTED | ||
except (OSError): | ||
LOGGER.debug("Failing config: %s", config) | ||
|
||
LOGGER.debug("No working config found") | ||
return RESULT_NOT_FOUND | ||
|
||
async def async_step_import(self, user_input=None): | ||
"""Handle configuration by yaml file.""" | ||
self._on_script = user_input.get(CONF_ON_ACTION) | ||
self._port = user_input.get(CONF_PORT) | ||
|
||
return await self.async_step_user(user_input) | ||
|
||
async def async_step_user(self, user_input=None): | ||
"""Handle a flow initialized by the user.""" | ||
if user_input is not None: | ||
ip_address = await self.hass.async_add_executor_job( | ||
_get_ip, user_input[CONF_HOST] | ||
) | ||
|
||
await self.async_set_unique_id(ip_address) | ||
self._abort_if_unique_id_configured() | ||
|
||
self._host = user_input.get(CONF_HOST) | ||
self._ip = self.context[CONF_IP_ADDRESS] = ip_address | ||
self._title = user_input.get(CONF_NAME) | ||
|
||
result = await self.hass.async_add_executor_job(self._try_connect) | ||
|
||
if result != RESULT_SUCCESS: | ||
return self.async_abort(reason=result) | ||
return self._get_entry() | ||
|
||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) | ||
|
||
async def async_step_ssdp(self, user_input=None): | ||
"""Handle a flow initialized by discovery.""" | ||
host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname | ||
ip_address = await self.hass.async_add_executor_job(_get_ip, host) | ||
|
||
self._host = host | ||
self._ip = self.context[CONF_IP_ADDRESS] = ip_address | ||
self._manufacturer = user_input[ATTR_UPNP_MANUFACTURER] | ||
self._model = user_input[ATTR_UPNP_MODEL_NAME] | ||
self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] | ||
if self._name.startswith("[TV]"): | ||
self._name = self._name[4:] | ||
self._title = f"{self._name} ({self._model})" | ||
self._uuid = user_input[ATTR_UPNP_UDN] | ||
if self._uuid.startswith("uuid:"): | ||
self._uuid = self._uuid[5:] | ||
|
||
config_entry = await self.async_set_unique_id(ip_address) | ||
if config_entry: | ||
config_entry.data[CONF_ID] = self._uuid | ||
config_entry.data[CONF_MANUFACTURER] = self._manufacturer | ||
config_entry.data[CONF_MODEL] = self._model | ||
self.hass.config_entries.async_update_entry(config_entry) | ||
return self.async_abort(reason="already_configured") | ||
|
||
return await self.async_step_confirm() | ||
|
||
async def async_step_confirm(self, user_input=None): | ||
"""Handle user-confirmation of discovered node.""" | ||
if user_input is not None: | ||
result = await self.hass.async_add_executor_job(self._try_connect) | ||
|
||
if result != RESULT_SUCCESS: | ||
return self.async_abort(reason=result) | ||
return self._get_entry() | ||
|
||
return self.async_show_form( | ||
step_id="confirm", description_placeholders={"model": self._model} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.