Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions homeassistant/components/samsungtv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""The Samsung TV integration."""
import socket

import voluptuous as vol

from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
Expand All @@ -13,7 +11,7 @@
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]
[entry[CONF_HOST] for entry in value]
)
return value

Expand Down Expand Up @@ -42,15 +40,11 @@ def ensure_unique_hosts(value):

async def async_setup(hass, config):
"""Set up the Samsung TV integration."""
hass.data[DOMAIN] = {}
if DOMAIN in config:
hass.data[DOMAIN] = {}
for entry_config in config[DOMAIN]:
ip_address = await hass.async_add_executor_job(
socket.gethostbyname, entry_config[CONF_HOST]
)
hass.data[DOMAIN][ip_address] = {
CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION)
}
host = entry_config[CONF_HOST]
hass.data[DOMAIN][host] = {CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION)}
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=entry_config
Expand Down
27 changes: 23 additions & 4 deletions homeassistant/components/samsungtv/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
from samsungtvws import SamsungTVWS
from samsungtvws.exceptions import ConnectionFailure
from samsungtvws.exceptions import ConnectionFailure, HttpApiError
from websocket import WebSocketException

from homeassistant.const import (
Expand All @@ -27,6 +27,7 @@
RESULT_SUCCESS,
VALUE_CONF_ID,
VALUE_CONF_NAME,
WEBSOCKET_PORTS,
)


Expand Down Expand Up @@ -58,6 +59,10 @@ def register_reauth_callback(self, func):
def try_connect(self):
"""Try to connect to the TV."""

@abstractmethod
def device_info(self):
"""Try to gather infos of this TV."""

def is_on(self):
"""Tells if the TV is on."""
self.close_remote()
Expand Down Expand Up @@ -166,6 +171,10 @@ def try_connect(self):
LOGGER.debug("Failing config: %s, error: %s", config, err)
return RESULT_NOT_SUCCESSFUL

def device_info(self):
"""Try to gather infos of this device."""
return None

def _get_remote(self):
"""Create or return a remote control instance."""
if self._remote is None:
Expand Down Expand Up @@ -196,7 +205,7 @@ def __init__(self, method, host, port, token=None):

def try_connect(self):
"""Try to connect to the Websocket TV."""
for self.port in (8001, 8002):
for self.port in WEBSOCKET_PORTS:
config = {
CONF_NAME: VALUE_CONF_NAME,
CONF_HOST: self.host,
Expand Down Expand Up @@ -234,6 +243,17 @@ def try_connect(self):

return RESULT_NOT_SUCCESSFUL

def device_info(self):
"""Try to gather infos of this TV."""
remote = self._get_remote()
if remote:
try:
return remote.rest_device_info()
except HttpApiError:
# unable to get, ignore
pass
return None

def _send_key(self, key):
"""Send the key using websocket protocol."""
if key == "KEY_POWEROFF":
Expand All @@ -258,7 +278,6 @@ def _get_remote(self):
# A removed auth will lead to socket timeout because waiting for auth popup is just an open socket
except ConnectionFailure:
self._notify_callback()
raise
except WebSocketException:
except (WebSocketException, OSError):
self._remote = None
return self._remote
164 changes: 111 additions & 53 deletions homeassistant/components/samsungtv/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""Config flow for Samsung TV."""
import socket
from socket import gethostbyname
from urllib.parse import urlparse

import voluptuous as vol

from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER,
ATTR_UPNP_MODEL_NAME,
ATTR_UPNP_UDN,
)
from homeassistant.components.zeroconf import ATTR_PROPERTIES
from homeassistant.const import (
CONF_HOST,
CONF_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_METHOD,
CONF_NAME,
CONF_PORT,
Expand All @@ -32,19 +34,15 @@
METHOD_WEBSOCKET,
RESULT_AUTH_MISSING,
RESULT_NOT_SUCCESSFUL,
RESULT_NOT_SUPPORTED,
RESULT_SUCCESS,
WEBSOCKET_PORTS,
)

DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str})
SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET]


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

Expand All @@ -56,19 +54,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize flow."""
self._host = None
self._ip = None
self._mac = None
self._manufacturer = None
self._model = None
self._name = None
self._title = None
self._id = None
self._bridge = None
self._device_info = None

def _get_entry(self):
data = {
CONF_HOST: self._host,
CONF_ID: self._id,
CONF_IP_ADDRESS: self._ip,
CONF_MAC: self._mac,
CONF_MANUFACTURER: self._manufacturer,
CONF_METHOD: self._bridge.method,
CONF_MODEL: self._model,
Expand All @@ -82,15 +80,71 @@ def _get_entry(self):
data=data,
)

async def _abort_if_already_configured(self):
device_ip = await self.hass.async_add_executor_job(gethostbyname, self._host)
for entry in self._async_current_entries():

# update user configured or unique_id=ip entries
if self._id and not entry.unique_id or device_ip == entry.unique_id:
data = {
key: value
for key, value in entry.data.items()
# clean up old entries
if key not in (CONF_ID, CONF_IP_ADDRESS)
}
if self._manufacturer and not data.get(CONF_MANUFACTURER):
data[CONF_MANUFACTURER] = self._manufacturer
if self._model and not data.get(CONF_MODEL):
data[CONF_MODEL] = self._model
self.hass.config_entries.async_update_entry(
entry, unique_id=self._id, data=data
)
raise data_entry_flow.AbortFlow("already_configured")

if (
self._host == entry.data[CONF_HOST]
or (self._id and self._id == entry.unique_id)
or (self._mac and self._mac == entry.data.get(CONF_MAC))
):
raise data_entry_flow.AbortFlow("already_configured")

def _try_connect(self):
"""Try to connect and check auth."""
for method in SUPPORTED_METHODS:
self._bridge = SamsungTVBridge.get_bridge(method, self._host)
result = self._bridge.try_connect()
if result == RESULT_SUCCESS:
return
if result != RESULT_NOT_SUCCESSFUL:
return result
raise data_entry_flow.AbortFlow(result)
LOGGER.debug("No working config found")
return RESULT_NOT_SUCCESSFUL
raise data_entry_flow.AbortFlow(RESULT_NOT_SUCCESSFUL)

async def _get_and_check_device_info(self):
"""Try to get the device info."""
if self._bridge:
self._device_info = await self.hass.async_add_executor_job(
self._bridge.device_info
)
else:
for port in WEBSOCKET_PORTS:
self._device_info = await self.hass.async_add_executor_job(
SamsungTVBridge.get_bridge(
METHOD_WEBSOCKET, self._host, port
).device_info
)
if self._device_info:
break

if self._device_info:
device_type = self._device_info.get("device", {}).get("type")
if device_type and device_type != "Samsung SmartTV":
raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
self._model = self._device_info.get("device", {}).get("modelName")
return

if self._bridge and self._bridge.method == METHOD_WEBSOCKET:
raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)

async def async_step_import(self, user_input=None):
"""Handle configuration by yaml file."""
Expand All @@ -99,81 +153,85 @@ async def async_step_import(self, user_input=None):
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._name = user_input.get(CONF_NAME)
self._host = user_input[CONF_HOST]
self._name = user_input[CONF_NAME]
self._title = self._name

result = await self.hass.async_add_executor_job(self._try_connect)
await self._abort_if_already_configured()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can only call this if you have set a unique ID. But that's not the case when the user instantiates this flow at this point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


await self.hass.async_add_executor_job(self._try_connect)
await self._get_and_check_device_info()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you set the unique ID after you get the device info?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique id call added


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, discovery_info):
"""Handle a flow initialized by discovery."""
host = urlparse(discovery_info[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 = discovery_info.get(ATTR_UPNP_MANUFACTURER)
self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME)
self._name = f"Samsung {self._model}"
self._id = discovery_info.get(ATTR_UPNP_UDN)
self._title = self._model
async def async_step_ssdp(self, user_input=None):
"""Handle a flow initialized by ssdp discovery."""
self._host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname
self._id = user_input.get(ATTR_UPNP_UDN)

# probably access denied
if self._id is None:
return self.async_abort(reason=RESULT_AUTH_MISSING)
if self._id.startswith("uuid:"):
self._id = self._id[5:]

await self.async_set_unique_id(ip_address)
self._abort_if_unique_id_configured(
{
CONF_ID: self._id,
CONF_MANUFACTURER: self._manufacturer,
CONF_MODEL: self._model,
}
)
await self.async_set_unique_id(self._id)
await self._get_and_check_device_info()

self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER)
if not self._model:
self._model = user_input.get(ATTR_UPNP_MODEL_NAME)
self._name = f"{self._manufacturer} {self._model}"
self._title = self._model

await self._abort_if_already_configured()

self.context["title_placeholders"] = {"model": self._model}
return await self.async_step_confirm()

async def async_step_zeroconf(self, user_input=None):
"""Handle a flow initialized by zeroconf discovery."""
self._host = user_input[CONF_HOST]
self._id = user_input[ATTR_PROPERTIES].get("serialNumber")

if self._id:
await self.async_set_unique_id(self._id)
await self._get_and_check_device_info()

self._mac = user_input[ATTR_PROPERTIES].get("deviceid")
self._manufacturer = user_input[ATTR_PROPERTIES].get("manufacturer")
if not self._model:
self._model = user_input[ATTR_PROPERTIES].get("model")
self._name = f"{self._manufacturer} {self._model}"
self._title = self._model

await self._abort_if_already_configured()

self.context["title_placeholders"] = {"model": self._model}
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)
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}
)

async def async_step_reauth(self, user_input=None):
async def async_step_reauth(self, user_input):
"""Handle configuration by re-auth."""
self._host = user_input[CONF_HOST]
self._id = user_input.get(CONF_ID)
self._ip = user_input[CONF_IP_ADDRESS]
self._manufacturer = user_input.get(CONF_MANUFACTURER)
self._model = user_input.get(CONF_MODEL)
self._name = user_input.get(CONF_NAME)
self._title = self._model or self._name

await self.async_set_unique_id(self._ip)
await self.async_set_unique_id(self.unique_id)
self.context["title_placeholders"] = {"model": self._title}

return await self.async_step_confirm()
3 changes: 3 additions & 0 deletions homeassistant/components/samsungtv/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CONF_MANUFACTURER = "manufacturer"
CONF_MODEL = "model"
CONF_ON_ACTION = "turn_on_action"
CONF_SERIALNO = "serial_number"

RESULT_AUTH_MISSING = "auth_missing"
RESULT_SUCCESS = "success"
Expand All @@ -21,3 +22,5 @@

METHOD_LEGACY = "legacy"
METHOD_WEBSOCKET = "websocket"

WEBSOCKET_PORTS = (8002, 8001)
Loading