Skip to content

Commit

Permalink
Add Samsung TV config flow (#28306)
Browse files Browse the repository at this point in the history
* 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
escoand authored and MartinHjelmare committed Jan 10, 2020
1 parent 4fb3645 commit ef05aa2
Show file tree
Hide file tree
Showing 14 changed files with 898 additions and 399 deletions.
1 change: 0 additions & 1 deletion homeassistant/components/discovery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
"logitech_mediaserver": ("media_player", "squeezebox"),
"directv": ("media_player", "directv"),
"denonavr": ("media_player", "denonavr"),
"samsung_tv": ("media_player", "samsungtv"),
"frontier_silicon": ("media_player", "frontier_silicon"),
"openhome": ("media_player", "openhome"),
"harmony": ("remote", "harmony"),
Expand Down
59 changes: 59 additions & 0 deletions homeassistant/components/samsungtv/__init__.py
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
184 changes: 184 additions & 0 deletions homeassistant/components/samsungtv/config_flow.py
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}
)
8 changes: 8 additions & 0 deletions homeassistant/components/samsungtv/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@

LOGGER = logging.getLogger(__package__)
DOMAIN = "samsungtv"

DEFAULT_NAME = "Samsung TV Remote"

CONF_MANUFACTURER = "manufacturer"
CONF_MODEL = "model"
CONF_ON_ACTION = "turn_on_action"

METHODS = ("websocket", "legacy")
14 changes: 12 additions & 2 deletions homeassistant/components/samsungtv/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
"domain": "samsungtv",
"name": "Samsung Smart TV",
"documentation": "https://www.home-assistant.io/integrations/samsungtv",
"requirements": ["samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6"],
"requirements": [
"samsungctl[websocket]==0.7.1"
],
"ssdp": [
{
"deviceType": "urn:samsung.com:device:RemoteControlReceiver:1"
}
],
"dependencies": [],
"codeowners": ["@escoand"]
"codeowners": [
"@escoand"
],
"config_flow": true
}
Loading

0 comments on commit ef05aa2

Please sign in to comment.