mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add Samsung TV config flow (#28306)
* 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 91502407eb216f8a0b1b90e3e6fb165b81406f8f. * 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 f37057819fd751a654779da743d0300751e963be. * 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
This commit is contained in:
parent
4fb36451c2
commit
ef05aa2f39
@ -75,7 +75,6 @@ SERVICE_HANDLERS = {
|
|||||||
"logitech_mediaserver": ("media_player", "squeezebox"),
|
"logitech_mediaserver": ("media_player", "squeezebox"),
|
||||||
"directv": ("media_player", "directv"),
|
"directv": ("media_player", "directv"),
|
||||||
"denonavr": ("media_player", "denonavr"),
|
"denonavr": ("media_player", "denonavr"),
|
||||||
"samsung_tv": ("media_player", "samsungtv"),
|
|
||||||
"frontier_silicon": ("media_player", "frontier_silicon"),
|
"frontier_silicon": ("media_player", "frontier_silicon"),
|
||||||
"openhome": ("media_player", "openhome"),
|
"openhome": ("media_player", "openhome"),
|
||||||
"harmony": ("remote", "harmony"),
|
"harmony": ("remote", "harmony"),
|
||||||
|
@ -1 +1,60 @@
|
|||||||
"""The Samsung TV integration."""
|
"""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
homeassistant/components/samsungtv/config_flow.py
Normal file
184
homeassistant/components/samsungtv/config_flow.py
Normal file
@ -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}
|
||||||
|
)
|
@ -3,3 +3,11 @@ import logging
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
DOMAIN = "samsungtv"
|
DOMAIN = "samsungtv"
|
||||||
|
|
||||||
|
DEFAULT_NAME = "Samsung TV Remote"
|
||||||
|
|
||||||
|
CONF_MANUFACTURER = "manufacturer"
|
||||||
|
CONF_MODEL = "model"
|
||||||
|
CONF_ON_ACTION = "turn_on_action"
|
||||||
|
|
||||||
|
METHODS = ("websocket", "legacy")
|
||||||
|
@ -2,7 +2,17 @@
|
|||||||
"domain": "samsungtv",
|
"domain": "samsungtv",
|
||||||
"name": "Samsung Smart TV",
|
"name": "Samsung Smart TV",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/samsungtv",
|
"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": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@escoand"]
|
"codeowners": [
|
||||||
|
"@escoand"
|
||||||
|
],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
"""Support for interface with an Samsung TV."""
|
"""Support for interface with an Samsung TV."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import socket
|
|
||||||
|
|
||||||
from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions
|
from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import wakeonlan
|
|
||||||
from websocket import WebSocketException
|
from websocket import WebSocketException
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice
|
||||||
DEVICE_CLASS_TV,
|
|
||||||
PLATFORM_SCHEMA,
|
|
||||||
MediaPlayerDevice,
|
|
||||||
)
|
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
MEDIA_TYPE_CHANNEL,
|
MEDIA_TYPE_CHANNEL,
|
||||||
SUPPORT_NEXT_TRACK,
|
SUPPORT_NEXT_TRACK,
|
||||||
@ -27,27 +21,20 @@ from homeassistant.components.media_player.const import (
|
|||||||
SUPPORT_VOLUME_STEP,
|
SUPPORT_VOLUME_STEP,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_BROADCAST_ADDRESS,
|
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MAC,
|
CONF_ID,
|
||||||
CONF_NAME,
|
CONF_METHOD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_TIMEOUT,
|
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.script import Script
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import CONF_MANUFACTURER, CONF_MODEL, CONF_ON_ACTION, DOMAIN, LOGGER
|
||||||
|
|
||||||
DEFAULT_NAME = "Samsung TV Remote"
|
|
||||||
DEFAULT_TIMEOUT = 1
|
|
||||||
DEFAULT_BROADCAST_ADDRESS = "255.255.255.255"
|
|
||||||
|
|
||||||
KEY_PRESS_TIMEOUT = 1.2
|
KEY_PRESS_TIMEOUT = 1.2
|
||||||
KNOWN_DEVICES_KEY = "samsungtv_known_devices"
|
|
||||||
METHODS = ("websocket", "legacy")
|
|
||||||
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
||||||
|
|
||||||
SUPPORT_SAMSUNGTV = (
|
SUPPORT_SAMSUNGTV = (
|
||||||
@ -62,73 +49,33 @@ SUPPORT_SAMSUNGTV = (
|
|||||||
| SUPPORT_PLAY_MEDIA
|
| SUPPORT_PLAY_MEDIA
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_HOST): cv.string,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_PORT): cv.port,
|
|
||||||
vol.Optional(CONF_MAC): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS
|
|
||||||
): cv.string,
|
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
hass, config, add_entities, discovery_info=None
|
||||||
|
): # pragma: no cover
|
||||||
"""Set up the Samsung TV platform."""
|
"""Set up the Samsung TV platform."""
|
||||||
known_devices = hass.data.get(KNOWN_DEVICES_KEY)
|
pass
|
||||||
if known_devices is None:
|
|
||||||
known_devices = set()
|
|
||||||
hass.data[KNOWN_DEVICES_KEY] = known_devices
|
|
||||||
|
|
||||||
uuid = None
|
|
||||||
# Is this a manual configuration?
|
|
||||||
if config.get(CONF_HOST) is not None:
|
|
||||||
host = config.get(CONF_HOST)
|
|
||||||
port = config.get(CONF_PORT)
|
|
||||||
name = config.get(CONF_NAME)
|
|
||||||
mac = config.get(CONF_MAC)
|
|
||||||
broadcast = config.get(CONF_BROADCAST_ADDRESS)
|
|
||||||
timeout = config.get(CONF_TIMEOUT)
|
|
||||||
elif discovery_info is not None:
|
|
||||||
tv_name = discovery_info.get("name")
|
|
||||||
model = discovery_info.get("model_name")
|
|
||||||
host = discovery_info.get("host")
|
|
||||||
name = f"{tv_name} ({model})"
|
|
||||||
if name.startswith("[TV]"):
|
|
||||||
name = name[4:]
|
|
||||||
port = None
|
|
||||||
timeout = DEFAULT_TIMEOUT
|
|
||||||
mac = None
|
|
||||||
broadcast = DEFAULT_BROADCAST_ADDRESS
|
|
||||||
uuid = discovery_info.get("udn")
|
|
||||||
if uuid and uuid.startswith("uuid:"):
|
|
||||||
uuid = uuid[len("uuid:") :]
|
|
||||||
|
|
||||||
# Only add a device once, so discovered devices do not override manual
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
# config.
|
"""Set up the Samsung TV from a config entry."""
|
||||||
ip_addr = socket.gethostbyname(host)
|
turn_on_action = config_entry.data.get(CONF_ON_ACTION)
|
||||||
if ip_addr not in known_devices:
|
on_script = Script(hass, turn_on_action) if turn_on_action else None
|
||||||
known_devices.add(ip_addr)
|
async_add_entities([SamsungTVDevice(config_entry, on_script)])
|
||||||
add_entities([SamsungTVDevice(host, port, name, timeout, mac, broadcast, uuid)])
|
|
||||||
LOGGER.info("Samsung TV %s added as '%s'", host, name)
|
|
||||||
else:
|
|
||||||
LOGGER.info("Ignoring duplicate Samsung TV %s", host)
|
|
||||||
|
|
||||||
|
|
||||||
class SamsungTVDevice(MediaPlayerDevice):
|
class SamsungTVDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Samsung TV."""
|
"""Representation of a Samsung TV."""
|
||||||
|
|
||||||
def __init__(self, host, port, name, timeout, mac, broadcast, uuid):
|
def __init__(self, config_entry, on_script):
|
||||||
"""Initialize the Samsung device."""
|
"""Initialize the Samsung device."""
|
||||||
|
self._config_entry = config_entry
|
||||||
# Save a reference to the imported classes
|
self._name = config_entry.title
|
||||||
self._name = name
|
self._uuid = config_entry.data.get(CONF_ID)
|
||||||
self._mac = mac
|
self._manufacturer = config_entry.data.get(CONF_MANUFACTURER)
|
||||||
self._broadcast = broadcast
|
self._model = config_entry.data.get(CONF_MODEL)
|
||||||
self._uuid = uuid
|
self._on_script = on_script
|
||||||
|
self._update_listener = None
|
||||||
# Assume that the TV is not muted
|
# Assume that the TV is not muted
|
||||||
self._muted = False
|
self._muted = False
|
||||||
# Assume that the TV is in Play mode
|
# Assume that the TV is in Play mode
|
||||||
@ -141,57 +88,20 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
# Generate a configuration for the Samsung library
|
# Generate a configuration for the Samsung library
|
||||||
self._config = {
|
self._config = {
|
||||||
"name": "HomeAssistant",
|
"name": "HomeAssistant",
|
||||||
"description": name,
|
"description": self._name,
|
||||||
"id": "ha.component.samsung",
|
"id": "ha.component.samsung",
|
||||||
"method": None,
|
"method": config_entry.data[CONF_METHOD],
|
||||||
"port": port,
|
"port": config_entry.data.get(CONF_PORT),
|
||||||
"host": host,
|
"host": config_entry.data[CONF_HOST],
|
||||||
"timeout": timeout,
|
"timeout": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Select method by port number, mainly for fallback
|
|
||||||
if self._config["port"] in (8001, 8002):
|
|
||||||
self._config["method"] = "websocket"
|
|
||||||
elif self._config["port"] == 55000:
|
|
||||||
self._config["method"] = "legacy"
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update state of device."""
|
"""Update state of device."""
|
||||||
self.send_key("KEY")
|
self.send_key("KEY")
|
||||||
|
|
||||||
def get_remote(self):
|
def get_remote(self):
|
||||||
"""Create or return a remote control instance."""
|
"""Create or return a remote control instance."""
|
||||||
|
|
||||||
# Try to find correct method automatically
|
|
||||||
if self._config["method"] not in METHODS:
|
|
||||||
for method in METHODS:
|
|
||||||
try:
|
|
||||||
self._config["method"] = method
|
|
||||||
LOGGER.debug("Try config: %s", self._config)
|
|
||||||
self._remote = SamsungRemote(self._config.copy())
|
|
||||||
self._state = STATE_ON
|
|
||||||
LOGGER.debug("Found working config: %s", self._config)
|
|
||||||
break
|
|
||||||
except (
|
|
||||||
samsung_exceptions.UnhandledResponse,
|
|
||||||
samsung_exceptions.AccessDenied,
|
|
||||||
):
|
|
||||||
# We got a response so it's working.
|
|
||||||
self._state = STATE_ON
|
|
||||||
LOGGER.debug(
|
|
||||||
"Found working config without connection: %s", self._config
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except OSError as err:
|
|
||||||
LOGGER.debug("Failing config: %s error was: %s", self._config, err)
|
|
||||||
self._config["method"] = None
|
|
||||||
|
|
||||||
# Unable to find working connection
|
|
||||||
if self._config["method"] is None:
|
|
||||||
self._remote = None
|
|
||||||
self._state = None
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self._remote is None:
|
if self._remote is None:
|
||||||
# We need to create a new instance to reconnect.
|
# We need to create a new instance to reconnect.
|
||||||
self._remote = SamsungRemote(self._config.copy())
|
self._remote = SamsungRemote(self._config.copy())
|
||||||
@ -219,9 +129,6 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
# WebSocketException can occur when timed out
|
# WebSocketException can occur when timed out
|
||||||
self._remote = None
|
self._remote = None
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
except AttributeError:
|
|
||||||
# Auto-detect could not find working config yet
|
|
||||||
pass
|
|
||||||
except (samsung_exceptions.UnhandledResponse, samsung_exceptions.AccessDenied):
|
except (samsung_exceptions.UnhandledResponse, samsung_exceptions.AccessDenied):
|
||||||
# We got a response so it's on.
|
# We got a response so it's on.
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
@ -256,6 +163,16 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"identifiers": {(DOMAIN, self.unique_id)},
|
||||||
|
"manufacturer": self._manufacturer,
|
||||||
|
"model": self._model,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self):
|
||||||
"""Boolean if volume is currently muted."""
|
"""Boolean if volume is currently muted."""
|
||||||
@ -269,7 +186,7 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag media player features that are supported."""
|
"""Flag media player features that are supported."""
|
||||||
if self._mac:
|
if self._on_script:
|
||||||
return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON
|
return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON
|
||||||
return SUPPORT_SAMSUNGTV
|
return SUPPORT_SAMSUNGTV
|
||||||
|
|
||||||
@ -344,21 +261,19 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for digit in media_id:
|
for digit in media_id:
|
||||||
await self.hass.async_add_job(self.send_key, f"KEY_{digit}")
|
await self.hass.async_add_executor_job(self.send_key, f"KEY_{digit}")
|
||||||
await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop)
|
await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop)
|
||||||
await self.hass.async_add_job(self.send_key, "KEY_ENTER")
|
await self.hass.async_add_executor_job(self.send_key, "KEY_ENTER")
|
||||||
|
|
||||||
def turn_on(self):
|
async def async_turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
if self._mac:
|
if self._on_script:
|
||||||
wakeonlan.send_magic_packet(self._mac, ip_address=self._broadcast)
|
await self._on_script.async_run()
|
||||||
else:
|
|
||||||
self.send_key("KEY_POWERON")
|
|
||||||
|
|
||||||
async def async_select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
if source not in SOURCES:
|
if source not in SOURCES:
|
||||||
LOGGER.error("Unsupported source")
|
LOGGER.error("Unsupported source")
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.hass.async_add_job(self.send_key, SOURCES[source])
|
self.send_key(SOURCES[source])
|
||||||
|
26
homeassistant/components/samsungtv/strings.json
Normal file
26
homeassistant/components/samsungtv/strings.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Samsung TV",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Samsung TV",
|
||||||
|
"description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.",
|
||||||
|
"data": {
|
||||||
|
"host": "Host or IP address",
|
||||||
|
"name": "Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"title": "Samsung TV",
|
||||||
|
"description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authentication. Manual configurations for this TV will be overwritten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_in_progress": "Samsung TV configuration is already in progress.",
|
||||||
|
"already_configured": "This Samsung TV is already configured.",
|
||||||
|
"auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.",
|
||||||
|
"not_found": "No supported Samsung TV devices found on the network.",
|
||||||
|
"not_supported": "This Samsung TV devices is currently not supported."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,7 @@ FLOWS = [
|
|||||||
"point",
|
"point",
|
||||||
"ps4",
|
"ps4",
|
||||||
"rainmachine",
|
"rainmachine",
|
||||||
|
"samsungtv",
|
||||||
"sentry",
|
"sentry",
|
||||||
"simplisafe",
|
"simplisafe",
|
||||||
"smartthings",
|
"smartthings",
|
||||||
|
@ -27,6 +27,11 @@ SSDP = {
|
|||||||
"manufacturer": "Royal Philips Electronics"
|
"manufacturer": "Royal Philips Electronics"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"samsungtv": [
|
||||||
|
{
|
||||||
|
"deviceType": "urn:samsung.com:device:RemoteControlReceiver:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
"sonos": [
|
"sonos": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||||
|
@ -2035,7 +2035,6 @@ vtjp==0.1.14
|
|||||||
vultr==0.1.2
|
vultr==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.panasonic_viera
|
# homeassistant.components.panasonic_viera
|
||||||
# homeassistant.components.samsungtv
|
|
||||||
# homeassistant.components.wake_on_lan
|
# homeassistant.components.wake_on_lan
|
||||||
wakeonlan==1.1.6
|
wakeonlan==1.1.6
|
||||||
|
|
||||||
|
@ -643,7 +643,6 @@ vsure==1.5.4
|
|||||||
vultr==0.1.2
|
vultr==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.panasonic_viera
|
# homeassistant.components.panasonic_viera
|
||||||
# homeassistant.components.samsungtv
|
|
||||||
# homeassistant.components.wake_on_lan
|
# homeassistant.components.wake_on_lan
|
||||||
wakeonlan==1.1.6
|
wakeonlan==1.1.6
|
||||||
|
|
||||||
|
388
tests/components/samsungtv/test_config_flow.py
Normal file
388
tests/components/samsungtv/test_config_flow.py
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
"""Tests for Samsung TV config flow."""
|
||||||
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
|
from asynctest import mock
|
||||||
|
import pytest
|
||||||
|
from samsungctl.exceptions import AccessDenied, UnhandledResponse
|
||||||
|
|
||||||
|
from homeassistant.components.samsungtv.const import (
|
||||||
|
CONF_MANUFACTURER,
|
||||||
|
CONF_MODEL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
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_METHOD, CONF_NAME
|
||||||
|
|
||||||
|
MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"}
|
||||||
|
MOCK_SSDP_DATA = {
|
||||||
|
ATTR_SSDP_LOCATION: "https://fake_host:12345/test",
|
||||||
|
ATTR_UPNP_FRIENDLY_NAME: "[TV]fake_name",
|
||||||
|
ATTR_UPNP_MANUFACTURER: "fake_manufacturer",
|
||||||
|
ATTR_UPNP_MODEL_NAME: "fake_model",
|
||||||
|
ATTR_UPNP_UDN: "uuid:fake_uuid",
|
||||||
|
}
|
||||||
|
MOCK_SSDP_DATA_NOPREFIX = {
|
||||||
|
ATTR_SSDP_LOCATION: "http://fake2_host:12345/test",
|
||||||
|
ATTR_UPNP_FRIENDLY_NAME: "fake2_name",
|
||||||
|
ATTR_UPNP_MANUFACTURER: "fake2_manufacturer",
|
||||||
|
ATTR_UPNP_MODEL_NAME: "fake2_model",
|
||||||
|
ATTR_UPNP_UDN: "fake2_uuid",
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTODETECT_WEBSOCKET = {
|
||||||
|
"name": "HomeAssistant",
|
||||||
|
"description": "HomeAssistant",
|
||||||
|
"id": "ha.component.samsung",
|
||||||
|
"method": "websocket",
|
||||||
|
"port": None,
|
||||||
|
"host": "fake_host",
|
||||||
|
"timeout": 1,
|
||||||
|
}
|
||||||
|
AUTODETECT_LEGACY = {
|
||||||
|
"name": "HomeAssistant",
|
||||||
|
"description": "HomeAssistant",
|
||||||
|
"id": "ha.component.samsung",
|
||||||
|
"method": "legacy",
|
||||||
|
"port": None,
|
||||||
|
"host": "fake_host",
|
||||||
|
"timeout": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="remote")
|
||||||
|
def remote_fixture():
|
||||||
|
"""Patch the samsungctl Remote."""
|
||||||
|
with patch("samsungctl.Remote") as remote_class, patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.socket"
|
||||||
|
) as socket_class:
|
||||||
|
remote = mock.Mock()
|
||||||
|
remote.__enter__ = mock.Mock()
|
||||||
|
remote.__exit__ = mock.Mock()
|
||||||
|
remote_class.return_value = remote
|
||||||
|
socket = mock.Mock()
|
||||||
|
socket_class.return_value = socket
|
||||||
|
yield remote
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user(hass, remote):
|
||||||
|
"""Test starting a flow by user."""
|
||||||
|
|
||||||
|
# show form
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
# entry was added
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "fake_name"
|
||||||
|
assert result["data"][CONF_HOST] == "fake_host"
|
||||||
|
assert result["data"][CONF_NAME] is None
|
||||||
|
assert result["data"][CONF_MANUFACTURER] is None
|
||||||
|
assert result["data"][CONF_MODEL] is None
|
||||||
|
assert result["data"][CONF_ID] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_missing_auth(hass):
|
||||||
|
"""Test starting a flow by user with authentication."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=AccessDenied("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# missing authentication
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "auth_missing"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_not_supported(hass):
|
||||||
|
"""Test starting a flow by user for not supported device."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=UnhandledResponse("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# device not supported
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_supported"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_not_found(hass):
|
||||||
|
"""Test starting a flow by user but no device found."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=OSError("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# device not found
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_already_configured(hass, remote):
|
||||||
|
"""Test starting a flow by user when already configured."""
|
||||||
|
|
||||||
|
# entry was added
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
|
||||||
|
# failed as already configured
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp(hass, remote):
|
||||||
|
"""Test starting a flow from discovery."""
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# entry was added
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "fake_name (fake_model)"
|
||||||
|
assert result["data"][CONF_HOST] == "fake_host"
|
||||||
|
assert result["data"][CONF_NAME] == "fake_name"
|
||||||
|
assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer"
|
||||||
|
assert result["data"][CONF_MODEL] == "fake_model"
|
||||||
|
assert result["data"][CONF_ID] == "fake_uuid"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_noprefix(hass, remote):
|
||||||
|
"""Test starting a flow from discovery without prefixes."""
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA_NOPREFIX
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# entry was added
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "fake2_name (fake2_model)"
|
||||||
|
assert result["data"][CONF_HOST] == "fake2_host"
|
||||||
|
assert result["data"][CONF_NAME] == "fake2_name"
|
||||||
|
assert result["data"][CONF_MANUFACTURER] == "fake2_manufacturer"
|
||||||
|
assert result["data"][CONF_MODEL] == "fake2_model"
|
||||||
|
assert result["data"][CONF_ID] == "fake2_uuid"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_missing_auth(hass):
|
||||||
|
"""Test starting a flow from discovery with authentication."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=AccessDenied("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# missing authentication
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "auth_missing"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_not_supported(hass):
|
||||||
|
"""Test starting a flow from discovery for not supported device."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=UnhandledResponse("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# device not supported
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_supported"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_not_found(hass):
|
||||||
|
"""Test starting a flow from discovery but no device found."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=OSError("Boom"),
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.socket"):
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# device not found
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_already_in_progress(hass, remote):
|
||||||
|
"""Test starting a flow from discovery twice."""
|
||||||
|
|
||||||
|
# confirm to add the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# failed as already in progress
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_in_progress"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp_already_configured(hass, remote):
|
||||||
|
"""Test starting a flow from discovery when already configured."""
|
||||||
|
|
||||||
|
# entry was added
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"][CONF_MANUFACTURER] is None
|
||||||
|
assert result["data"][CONF_MODEL] is None
|
||||||
|
assert result["data"][CONF_ID] is None
|
||||||
|
|
||||||
|
# failed as already configured
|
||||||
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
|
||||||
|
)
|
||||||
|
assert result2["type"] == "abort"
|
||||||
|
assert result2["reason"] == "already_configured"
|
||||||
|
|
||||||
|
# check updated device info
|
||||||
|
assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer"
|
||||||
|
assert result["data"][CONF_MODEL] == "fake_model"
|
||||||
|
assert result["data"][CONF_ID] == "fake_uuid"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_autodetect_websocket(hass, remote):
|
||||||
|
"""Test for send key with autodetection of protocol."""
|
||||||
|
with patch("homeassistant.components.samsungtv.config_flow.Remote") as remote:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"][CONF_METHOD] == "websocket"
|
||||||
|
assert remote.call_count == 1
|
||||||
|
assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_autodetect_auth_missing(hass, remote):
|
||||||
|
"""Test for send key with autodetection of protocol."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=[AccessDenied("Boom")],
|
||||||
|
) as remote:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "auth_missing"
|
||||||
|
assert remote.call_count == 1
|
||||||
|
assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_autodetect_not_supported(hass, remote):
|
||||||
|
"""Test for send key with autodetection of protocol."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=[UnhandledResponse("Boom")],
|
||||||
|
) as remote:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_supported"
|
||||||
|
assert remote.call_count == 1
|
||||||
|
assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_autodetect_legacy(hass, remote):
|
||||||
|
"""Test for send key with autodetection of protocol."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=[OSError("Boom"), mock.DEFAULT],
|
||||||
|
) as remote:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"][CONF_METHOD] == "legacy"
|
||||||
|
assert remote.call_count == 2
|
||||||
|
assert remote.call_args_list == [
|
||||||
|
call(AUTODETECT_WEBSOCKET),
|
||||||
|
call(AUTODETECT_LEGACY),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_autodetect_none(hass, remote):
|
||||||
|
"""Test for send key with autodetection of protocol."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=OSError("Boom"),
|
||||||
|
) as remote:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "not_found"
|
||||||
|
assert remote.call_count == 2
|
||||||
|
assert remote.call_args_list == [
|
||||||
|
call(AUTODETECT_WEBSOCKET),
|
||||||
|
call(AUTODETECT_LEGACY),
|
||||||
|
]
|
97
tests/components/samsungtv/test_init.py
Normal file
97
tests/components/samsungtv/test_init.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
"""Tests for the Samsung TV Integration."""
|
||||||
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON
|
||||||
|
from homeassistant.components.samsungtv.const import (
|
||||||
|
CONF_ON_ACTION,
|
||||||
|
DOMAIN as SAMSUNGTV_DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_SUPPORTED_FEATURES,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PORT,
|
||||||
|
SERVICE_VOLUME_UP,
|
||||||
|
)
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
ENTITY_ID = f"{DOMAIN}.fake_name"
|
||||||
|
MOCK_CONFIG = {
|
||||||
|
SAMSUNGTV_DOMAIN: [
|
||||||
|
{
|
||||||
|
CONF_HOST: "fake_host",
|
||||||
|
CONF_NAME: "fake_name",
|
||||||
|
CONF_PORT: 1234,
|
||||||
|
CONF_ON_ACTION: [{"delay": "00:00:01"}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
REMOTE_CALL = {
|
||||||
|
"name": "HomeAssistant",
|
||||||
|
"description": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_NAME],
|
||||||
|
"id": "ha.component.samsung",
|
||||||
|
"method": "websocket",
|
||||||
|
"port": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_PORT],
|
||||||
|
"host": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_HOST],
|
||||||
|
"timeout": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="remote")
|
||||||
|
def remote_fixture():
|
||||||
|
"""Patch the samsungctl Remote."""
|
||||||
|
with patch("homeassistant.components.samsungtv.socket"), patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.socket"
|
||||||
|
), patch("homeassistant.components.samsungtv.config_flow.Remote"), patch(
|
||||||
|
"homeassistant.components.samsungtv.media_player.SamsungRemote"
|
||||||
|
) as remote:
|
||||||
|
yield remote
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup(hass, remote):
|
||||||
|
"""Test Samsung TV integration is setup."""
|
||||||
|
await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
|
||||||
|
# test name and turn_on
|
||||||
|
assert state
|
||||||
|
assert state.name == "fake_name"
|
||||||
|
assert (
|
||||||
|
state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON
|
||||||
|
)
|
||||||
|
|
||||||
|
# test host and port
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||||
|
)
|
||||||
|
assert remote.mock_calls[0] == call(REMOTE_CALL)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_duplicate_config(hass, remote, caplog):
|
||||||
|
"""Test duplicate setup of platform."""
|
||||||
|
DUPLICATE = {
|
||||||
|
SAMSUNGTV_DOMAIN: [
|
||||||
|
MOCK_CONFIG[SAMSUNGTV_DOMAIN][0],
|
||||||
|
MOCK_CONFIG[SAMSUNGTV_DOMAIN][0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await async_setup_component(hass, SAMSUNGTV_DOMAIN, DUPLICATE)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(ENTITY_ID) is None
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
assert "duplicate host entries found" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_duplicate_entries(hass, remote, caplog):
|
||||||
|
"""Test duplicate setup of platform."""
|
||||||
|
await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(ENTITY_ID)
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG)
|
||||||
|
assert len(hass.states.async_all()) == 1
|
@ -2,9 +2,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import call, patch
|
|
||||||
|
|
||||||
from asynctest import mock
|
from asynctest import mock
|
||||||
|
from asynctest.mock import call, patch
|
||||||
import pytest
|
import pytest
|
||||||
from samsungctl import exceptions
|
from samsungctl import exceptions
|
||||||
from websocket import WebSocketException
|
from websocket import WebSocketException
|
||||||
@ -22,21 +22,18 @@ from homeassistant.components.media_player.const import (
|
|||||||
SERVICE_SELECT_SOURCE,
|
SERVICE_SELECT_SOURCE,
|
||||||
SUPPORT_TURN_ON,
|
SUPPORT_TURN_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN
|
from homeassistant.components.samsungtv.const import (
|
||||||
from homeassistant.components.samsungtv.media_player import (
|
CONF_ON_ACTION,
|
||||||
CONF_TIMEOUT,
|
DOMAIN as SAMSUNGTV_DOMAIN,
|
||||||
SUPPORT_SAMSUNGTV,
|
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
CONF_BROADCAST_ADDRESS,
|
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MAC,
|
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PLATFORM,
|
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
SERVICE_MEDIA_NEXT_TRACK,
|
SERVICE_MEDIA_NEXT_TRACK,
|
||||||
SERVICE_MEDIA_PAUSE,
|
SERVICE_MEDIA_PAUSE,
|
||||||
@ -49,9 +46,7 @@ from homeassistant.const import (
|
|||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_UNKNOWN,
|
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -59,107 +54,46 @@ from tests.common import async_fire_time_changed
|
|||||||
|
|
||||||
ENTITY_ID = f"{DOMAIN}.fake"
|
ENTITY_ID = f"{DOMAIN}.fake"
|
||||||
MOCK_CONFIG = {
|
MOCK_CONFIG = {
|
||||||
DOMAIN: {
|
SAMSUNGTV_DOMAIN: [
|
||||||
CONF_PLATFORM: SAMSUNGTV_DOMAIN,
|
{
|
||||||
CONF_HOST: "fake",
|
CONF_HOST: "fake",
|
||||||
CONF_NAME: "fake",
|
CONF_NAME: "fake",
|
||||||
CONF_PORT: 8001,
|
CONF_PORT: 8001,
|
||||||
CONF_TIMEOUT: 10,
|
CONF_ON_ACTION: [{"delay": "00:00:01"}],
|
||||||
CONF_MAC: "38:f9:d3:82:b4:f1",
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
ENTITY_ID_BROADCAST = f"{DOMAIN}.fake_broadcast"
|
ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon"
|
||||||
MOCK_CONFIG_BROADCAST = {
|
MOCK_CONFIG_NOTURNON = {
|
||||||
DOMAIN: {
|
SAMSUNGTV_DOMAIN: [
|
||||||
CONF_PLATFORM: SAMSUNGTV_DOMAIN,
|
{CONF_HOST: "fake_noturnon", CONF_NAME: "fake_noturnon", CONF_PORT: 55000}
|
||||||
CONF_HOST: "fake_broadcast",
|
]
|
||||||
CONF_NAME: "fake_broadcast",
|
|
||||||
CONF_PORT: 8001,
|
|
||||||
CONF_TIMEOUT: 10,
|
|
||||||
CONF_MAC: "38:f9:d3:82:b4:f1",
|
|
||||||
CONF_BROADCAST_ADDRESS: "192.168.5.255",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac"
|
|
||||||
MOCK_CONFIG_NOMAC = {
|
|
||||||
DOMAIN: {
|
|
||||||
CONF_PLATFORM: SAMSUNGTV_DOMAIN,
|
|
||||||
CONF_HOST: "fake_nomac",
|
|
||||||
CONF_NAME: "fake_nomac",
|
|
||||||
CONF_PORT: 55000,
|
|
||||||
CONF_TIMEOUT: 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ENTITY_ID_AUTO = f"{DOMAIN}.fake_auto"
|
|
||||||
MOCK_CONFIG_AUTO = {
|
|
||||||
DOMAIN: {
|
|
||||||
CONF_PLATFORM: SAMSUNGTV_DOMAIN,
|
|
||||||
CONF_HOST: "fake_auto",
|
|
||||||
CONF_NAME: "fake_auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ENTITY_ID_DISCOVERY = f"{DOMAIN}.fake_discovery_fake_model"
|
|
||||||
MOCK_CONFIG_DISCOVERY = {
|
|
||||||
"name": "fake_discovery",
|
|
||||||
"model_name": "fake_model",
|
|
||||||
"host": "fake_host",
|
|
||||||
"udn": "fake_uuid",
|
|
||||||
}
|
|
||||||
|
|
||||||
ENTITY_ID_DISCOVERY_PREFIX = f"{DOMAIN}.fake_discovery_prefix_fake_model_prefix"
|
|
||||||
MOCK_CONFIG_DISCOVERY_PREFIX = {
|
|
||||||
"name": "[TV]fake_discovery_prefix",
|
|
||||||
"model_name": "fake_model_prefix",
|
|
||||||
"host": "fake_host_prefix",
|
|
||||||
"udn": "uuid:fake_uuid_prefix",
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTODETECT_WEBSOCKET = {
|
|
||||||
"name": "HomeAssistant",
|
|
||||||
"description": "fake_auto",
|
|
||||||
"id": "ha.component.samsung",
|
|
||||||
"method": "websocket",
|
|
||||||
"port": None,
|
|
||||||
"host": "fake_auto",
|
|
||||||
"timeout": 1,
|
|
||||||
}
|
|
||||||
AUTODETECT_LEGACY = {
|
|
||||||
"name": "HomeAssistant",
|
|
||||||
"description": "fake_auto",
|
|
||||||
"id": "ha.component.samsung",
|
|
||||||
"method": "legacy",
|
|
||||||
"port": None,
|
|
||||||
"host": "fake_auto",
|
|
||||||
"timeout": 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="remote")
|
@pytest.fixture(name="remote")
|
||||||
def remote_fixture():
|
def remote_fixture():
|
||||||
"""Patch the samsungctl Remote."""
|
"""Patch the samsungctl Remote."""
|
||||||
with patch(
|
with patch("homeassistant.components.samsungtv.config_flow.socket"), patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote"
|
||||||
|
), patch(
|
||||||
"homeassistant.components.samsungtv.media_player.SamsungRemote"
|
"homeassistant.components.samsungtv.media_player.SamsungRemote"
|
||||||
) as remote_class, patch(
|
) as remote_class, patch(
|
||||||
"homeassistant.components.samsungtv.media_player.socket"
|
"homeassistant.components.samsungtv.socket"
|
||||||
) as socket_class:
|
):
|
||||||
remote = mock.Mock()
|
remote = mock.Mock()
|
||||||
remote_class.return_value = remote
|
remote_class.return_value = remote
|
||||||
socket = mock.Mock()
|
|
||||||
socket_class.return_value = socket
|
|
||||||
yield remote
|
yield remote
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="wakeonlan")
|
@pytest.fixture(name="delay")
|
||||||
def wakeonlan_fixture():
|
def delay_fixture():
|
||||||
"""Patch the wakeonlan Remote."""
|
"""Patch the delay script function."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.samsungtv.media_player.wakeonlan"
|
"homeassistant.components.samsungtv.media_player.Script.async_run"
|
||||||
) as wakeonlan_module:
|
) as delay:
|
||||||
yield wakeonlan_module
|
yield delay
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -170,61 +104,20 @@ def mock_now():
|
|||||||
|
|
||||||
async def setup_samsungtv(hass, config):
|
async def setup_samsungtv(hass, config):
|
||||||
"""Set up mock Samsung TV."""
|
"""Set up mock Samsung TV."""
|
||||||
await async_setup_component(hass, "media_player", config)
|
await async_setup_component(hass, SAMSUNGTV_DOMAIN, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_with_mac(hass, remote):
|
async def test_setup_with_turnon(hass, remote):
|
||||||
"""Test setup of platform."""
|
"""Test setup of platform."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
assert hass.states.get(ENTITY_ID)
|
assert hass.states.get(ENTITY_ID)
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_duplicate(hass, remote, caplog):
|
async def test_setup_without_turnon(hass, remote):
|
||||||
"""Test duplicate setup of platform."""
|
|
||||||
DUPLICATE = {DOMAIN: [MOCK_CONFIG[DOMAIN], MOCK_CONFIG[DOMAIN]]}
|
|
||||||
await setup_samsungtv(hass, DUPLICATE)
|
|
||||||
assert "Ignoring duplicate Samsung TV fake" in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_without_mac(hass, remote):
|
|
||||||
"""Test setup of platform."""
|
"""Test setup of platform."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_NOMAC)
|
await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON)
|
||||||
assert hass.states.get(ENTITY_ID_NOMAC)
|
assert hass.states.get(ENTITY_ID_NOTURNON)
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_discovery(hass, remote):
|
|
||||||
"""Test setup of platform with discovery."""
|
|
||||||
hass.async_create_task(
|
|
||||||
async_load_platform(
|
|
||||||
hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY, {DOMAIN: {}}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(ENTITY_ID_DISCOVERY)
|
|
||||||
assert state
|
|
||||||
assert state.name == "fake_discovery (fake_model)"
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
|
||||||
entry = entity_registry.async_get(ENTITY_ID_DISCOVERY)
|
|
||||||
assert entry
|
|
||||||
assert entry.unique_id == "fake_uuid"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_discovery_prefix(hass, remote):
|
|
||||||
"""Test setup of platform with discovery."""
|
|
||||||
hass.async_create_task(
|
|
||||||
async_load_platform(
|
|
||||||
hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY_PREFIX, {DOMAIN: {}}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(ENTITY_ID_DISCOVERY_PREFIX)
|
|
||||||
assert state
|
|
||||||
assert state.name == "fake_discovery_prefix (fake_model_prefix)"
|
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
|
||||||
entry = entity_registry.async_get(ENTITY_ID_DISCOVERY_PREFIX)
|
|
||||||
assert entry
|
|
||||||
assert entry.unique_id == "fake_uuid_prefix"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_update_on(hass, remote, mock_now):
|
async def test_update_on(hass, remote, mock_now):
|
||||||
@ -254,7 +147,7 @@ async def test_update_off(hass, remote, mock_now):
|
|||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key(hass, remote, wakeonlan):
|
async def test_send_key(hass, remote):
|
||||||
"""Test for send key."""
|
"""Test for send key."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
@ -267,85 +160,6 @@ async def test_send_key(hass, remote, wakeonlan):
|
|||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key_autodetect_websocket(hass, remote):
|
|
||||||
"""Test for send key with autodetection of protocol."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.samsungtv.media_player.SamsungRemote"
|
|
||||||
) as remote, patch("homeassistant.components.samsungtv.media_player.socket"):
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_AUTO)
|
|
||||||
assert await hass.services.async_call(
|
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True
|
|
||||||
)
|
|
||||||
state = hass.states.get(ENTITY_ID_AUTO)
|
|
||||||
assert remote.call_count == 1
|
|
||||||
assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)]
|
|
||||||
assert state.state == STATE_ON
|
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key_autodetect_websocket_exception(hass, caplog):
|
|
||||||
"""Test for send key with autodetection of protocol."""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.samsungtv.media_player.SamsungRemote",
|
|
||||||
side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT],
|
|
||||||
) as remote, patch("homeassistant.components.samsungtv.media_player.socket"):
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_AUTO)
|
|
||||||
assert await hass.services.async_call(
|
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True
|
|
||||||
)
|
|
||||||
state = hass.states.get(ENTITY_ID_AUTO)
|
|
||||||
# called 2 times because of the exception and the send key
|
|
||||||
assert remote.call_count == 2
|
|
||||||
assert remote.call_args_list == [
|
|
||||||
call(AUTODETECT_WEBSOCKET),
|
|
||||||
call(AUTODETECT_WEBSOCKET),
|
|
||||||
]
|
|
||||||
assert state.state == STATE_ON
|
|
||||||
assert "Found working config without connection: " in caplog.text
|
|
||||||
assert "Failing config: " not in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key_autodetect_legacy(hass, remote):
|
|
||||||
"""Test for send key with autodetection of protocol."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.samsungtv.media_player.SamsungRemote",
|
|
||||||
side_effect=[OSError("Boom"), mock.DEFAULT],
|
|
||||||
) as remote, patch("homeassistant.components.samsungtv.media_player.socket"):
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_AUTO)
|
|
||||||
assert await hass.services.async_call(
|
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True
|
|
||||||
)
|
|
||||||
state = hass.states.get(ENTITY_ID_AUTO)
|
|
||||||
assert remote.call_count == 2
|
|
||||||
assert remote.call_args_list == [
|
|
||||||
call(AUTODETECT_WEBSOCKET),
|
|
||||||
call(AUTODETECT_LEGACY),
|
|
||||||
]
|
|
||||||
assert state.state == STATE_ON
|
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key_autodetect_none(hass, remote):
|
|
||||||
"""Test for send key with autodetection of protocol."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.samsungtv.media_player.SamsungRemote",
|
|
||||||
side_effect=OSError("Boom"),
|
|
||||||
) as remote, patch("homeassistant.components.samsungtv.media_player.socket"):
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_AUTO)
|
|
||||||
assert await hass.services.async_call(
|
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True
|
|
||||||
)
|
|
||||||
state = hass.states.get(ENTITY_ID_AUTO)
|
|
||||||
# 4 calls because of retry
|
|
||||||
assert remote.call_count == 4
|
|
||||||
assert remote.call_args_list == [
|
|
||||||
call(AUTODETECT_WEBSOCKET),
|
|
||||||
call(AUTODETECT_LEGACY),
|
|
||||||
call(AUTODETECT_WEBSOCKET),
|
|
||||||
call(AUTODETECT_LEGACY),
|
|
||||||
]
|
|
||||||
assert state.state == STATE_UNKNOWN
|
|
||||||
|
|
||||||
|
|
||||||
async def test_send_key_broken_pipe(hass, remote):
|
async def test_send_key_broken_pipe(hass, remote):
|
||||||
"""Testing broken pipe Exception."""
|
"""Testing broken pipe Exception."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
@ -417,7 +231,7 @@ async def test_name(hass, remote):
|
|||||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake"
|
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake"
|
||||||
|
|
||||||
|
|
||||||
async def test_state_with_mac(hass, remote, wakeonlan):
|
async def test_state_with_turnon(hass, remote, delay):
|
||||||
"""Test for state property."""
|
"""Test for state property."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
@ -425,6 +239,8 @@ async def test_state_with_mac(hass, remote, wakeonlan):
|
|||||||
)
|
)
|
||||||
state = hass.states.get(ENTITY_ID)
|
state = hass.states.get(ENTITY_ID)
|
||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
assert delay.call_count == 1
|
||||||
|
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||||
)
|
)
|
||||||
@ -432,22 +248,22 @@ async def test_state_with_mac(hass, remote, wakeonlan):
|
|||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
async def test_state_without_mac(hass, remote):
|
async def test_state_without_turnon(hass, remote):
|
||||||
"""Test for state property."""
|
"""Test for state property."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_NOMAC)
|
await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True
|
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True
|
||||||
)
|
)
|
||||||
state = hass.states.get(ENTITY_ID_NOMAC)
|
state = hass.states.get(ENTITY_ID_NOTURNON)
|
||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True
|
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True
|
||||||
)
|
)
|
||||||
state = hass.states.get(ENTITY_ID_NOMAC)
|
state = hass.states.get(ENTITY_ID_NOTURNON)
|
||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
async def test_supported_features_with_mac(hass, remote):
|
async def test_supported_features_with_turnon(hass, remote):
|
||||||
"""Test for supported_features property."""
|
"""Test for supported_features property."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
state = hass.states.get(ENTITY_ID)
|
state = hass.states.get(ENTITY_ID)
|
||||||
@ -456,10 +272,10 @@ async def test_supported_features_with_mac(hass, remote):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_supported_features_without_mac(hass, remote):
|
async def test_supported_features_without_turnon(hass, remote):
|
||||||
"""Test for supported_features property."""
|
"""Test for supported_features property."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_NOMAC)
|
await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON)
|
||||||
state = hass.states.get(ENTITY_ID_NOMAC)
|
state = hass.states.get(ENTITY_ID_NOTURNON)
|
||||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV
|
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV
|
||||||
|
|
||||||
|
|
||||||
@ -481,11 +297,21 @@ async def test_turn_off_websocket(hass, remote):
|
|||||||
assert remote.control.call_args_list == [call("KEY_POWER")]
|
assert remote.control.call_args_list == [call("KEY_POWER")]
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_off_legacy(hass, remote):
|
async def test_turn_off_legacy(hass):
|
||||||
"""Test for turn_off."""
|
"""Test for turn_off."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_NOMAC)
|
with patch("homeassistant.components.samsungtv.config_flow.socket"), patch(
|
||||||
|
"homeassistant.components.samsungtv.config_flow.Remote",
|
||||||
|
side_effect=[OSError("Boom"), mock.DEFAULT],
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.samsungtv.media_player.SamsungRemote"
|
||||||
|
) as remote_class, patch(
|
||||||
|
"homeassistant.components.samsungtv.socket"
|
||||||
|
):
|
||||||
|
remote = mock.Mock()
|
||||||
|
remote_class.return_value = remote
|
||||||
|
await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True
|
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True
|
||||||
)
|
)
|
||||||
# key called
|
# key called
|
||||||
assert remote.control.call_count == 1
|
assert remote.control.call_count == 1
|
||||||
@ -583,37 +409,20 @@ async def test_media_previous_track(hass, remote):
|
|||||||
assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")]
|
assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")]
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on_with_mac(hass, remote, wakeonlan):
|
async def test_turn_on_with_turnon(hass, remote, delay):
|
||||||
"""Test turn on."""
|
"""Test turn on."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||||
)
|
)
|
||||||
# key and update called
|
assert delay.call_count == 1
|
||||||
assert wakeonlan.send_magic_packet.call_count == 1
|
|
||||||
assert wakeonlan.send_magic_packet.call_args_list == [
|
|
||||||
call("38:f9:d3:82:b4:f1", ip_address="255.255.255.255")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on_with_mac_and_broadcast(hass, remote, wakeonlan):
|
async def test_turn_on_without_turnon(hass, remote):
|
||||||
"""Test turn on."""
|
"""Test turn on."""
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_BROADCAST)
|
await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_BROADCAST}, True
|
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True
|
||||||
)
|
|
||||||
# key and update called
|
|
||||||
assert wakeonlan.send_magic_packet.call_count == 1
|
|
||||||
assert wakeonlan.send_magic_packet.call_args_list == [
|
|
||||||
call("38:f9:d3:82:b4:f1", ip_address="192.168.5.255")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on_without_mac(hass, remote):
|
|
||||||
"""Test turn on."""
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG_NOMAC)
|
|
||||||
assert await hass.services.async_call(
|
|
||||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True
|
|
||||||
)
|
)
|
||||||
# nothing called as not supported feature
|
# nothing called as not supported feature
|
||||||
assert remote.control.call_count == 0
|
assert remote.control.call_count == 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user