mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Add support for finding the samsungtv MainTvAgent service location (#68763)
This commit is contained in:
parent
23c47c2206
commit
6cec53bea1
@ -5,11 +5,13 @@ from collections.abc import Mapping
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import getmac
|
import getmac
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import ssdp
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
@ -24,6 +26,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info
|
from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info
|
||||||
@ -31,12 +34,17 @@ from .const import (
|
|||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_ON_ACTION,
|
CONF_ON_ACTION,
|
||||||
CONF_SESSION_ID,
|
CONF_SESSION_ID,
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
|
||||||
|
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
ENTRY_RELOAD_COOLDOWN,
|
||||||
LEGACY_PORT,
|
LEGACY_PORT,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
METHOD_ENCRYPTED_WEBSOCKET,
|
METHOD_ENCRYPTED_WEBSOCKET,
|
||||||
METHOD_LEGACY,
|
METHOD_LEGACY,
|
||||||
|
UPNP_SVC_MAIN_TV_AGENT,
|
||||||
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +117,60 @@ def _async_get_device_bridge(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DebouncedEntryReloader:
|
||||||
|
"""Reload only after the timer expires."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Init the debounced entry reloader."""
|
||||||
|
self.hass = hass
|
||||||
|
self.entry = entry
|
||||||
|
self.token = self.entry.data.get(CONF_TOKEN)
|
||||||
|
self._debounced_reload = Debouncer(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
cooldown=ENTRY_RELOAD_COOLDOWN,
|
||||||
|
immediate=False,
|
||||||
|
function=self._async_reload_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_call(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Start the countdown for a reload."""
|
||||||
|
if (new_token := entry.data.get(CONF_TOKEN)) != self.token:
|
||||||
|
LOGGER.debug("Skipping reload as its a token update")
|
||||||
|
self.token = new_token
|
||||||
|
return # Token updates should not trigger a reload
|
||||||
|
LOGGER.debug("Calling debouncer to get a reload after cooldown")
|
||||||
|
await self._debounced_reload.async_call()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_cancel(self) -> None:
|
||||||
|
"""Cancel any pending reload."""
|
||||||
|
self._debounced_reload.async_cancel()
|
||||||
|
|
||||||
|
async def _async_reload_entry(self) -> None:
|
||||||
|
"""Reload entry."""
|
||||||
|
LOGGER.debug("Reloading entry %s", self.entry.title)
|
||||||
|
await self.hass.config_entries.async_reload(self.entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_update_ssdp_locations(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update ssdp locations from discovery cache."""
|
||||||
|
updates = {}
|
||||||
|
for ssdp_st, key in (
|
||||||
|
(UPNP_SVC_RENDERING_CONTROL, CONF_SSDP_RENDERING_CONTROL_LOCATION),
|
||||||
|
(UPNP_SVC_MAIN_TV_AGENT, CONF_SSDP_MAIN_TV_AGENT_LOCATION),
|
||||||
|
):
|
||||||
|
for discovery_info in await ssdp.async_get_discovery_info_by_st(hass, ssdp_st):
|
||||||
|
location = discovery_info.ssdp_location
|
||||||
|
host = urlparse(location).hostname
|
||||||
|
if host == entry.data[CONF_HOST]:
|
||||||
|
updates[key] = location
|
||||||
|
break
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
hass.config_entries.async_update_entry(entry, data={**entry.data, **updates})
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up the Samsung TV platform."""
|
"""Set up the Samsung TV platform."""
|
||||||
|
|
||||||
@ -128,14 +190,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
bridge.register_update_config_entry_callback(_update_config_entry)
|
bridge.register_update_config_entry_callback(_update_config_entry)
|
||||||
|
|
||||||
# Allow bridge to force the reload of the config_entry
|
|
||||||
@callback
|
|
||||||
def _reload_config_entry() -> None:
|
|
||||||
"""Update config entry with the new token."""
|
|
||||||
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
|
||||||
|
|
||||||
bridge.register_reload_callback(_reload_config_entry)
|
|
||||||
|
|
||||||
async def stop_bridge(event: Event) -> None:
|
async def stop_bridge(event: Event) -> None:
|
||||||
"""Stop SamsungTV bridge connection."""
|
"""Stop SamsungTV bridge connection."""
|
||||||
LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host)
|
LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host)
|
||||||
@ -145,8 +199,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await _async_update_ssdp_locations(hass, entry)
|
||||||
|
|
||||||
|
# We must not await after we setup the reload or there
|
||||||
|
# will be a race where the config flow will see the entry
|
||||||
|
# as not loaded and may reload it
|
||||||
|
debounced_reloader = DebouncedEntryReloader(hass, entry)
|
||||||
|
entry.async_on_unload(debounced_reloader.async_cancel)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call))
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = bridge
|
hass.data[DOMAIN][entry.entry_id] = bridge
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@ class SamsungTVBridge(ABC):
|
|||||||
self.token: str | None = None
|
self.token: str | None = None
|
||||||
self.session_id: str | None = None
|
self.session_id: str | None = None
|
||||||
self._reauth_callback: CALLBACK_TYPE | None = None
|
self._reauth_callback: CALLBACK_TYPE | None = None
|
||||||
self._reload_callback: CALLBACK_TYPE | None = None
|
|
||||||
self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None
|
self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None
|
||||||
self._app_list_callback: Callable[[dict[str, str]], None] | None = None
|
self._app_list_callback: Callable[[dict[str, str]], None] | None = None
|
||||||
|
|
||||||
@ -156,10 +155,6 @@ class SamsungTVBridge(ABC):
|
|||||||
"""Register a callback function."""
|
"""Register a callback function."""
|
||||||
self._reauth_callback = func
|
self._reauth_callback = func
|
||||||
|
|
||||||
def register_reload_callback(self, func: CALLBACK_TYPE) -> None:
|
|
||||||
"""Register a callback function."""
|
|
||||||
self._reload_callback = func
|
|
||||||
|
|
||||||
def register_update_config_entry_callback(
|
def register_update_config_entry_callback(
|
||||||
self, func: Callable[[Mapping[str, Any]], None]
|
self, func: Callable[[Mapping[str, Any]], None]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -211,11 +206,6 @@ class SamsungTVBridge(ABC):
|
|||||||
if self._reauth_callback is not None:
|
if self._reauth_callback is not None:
|
||||||
self._reauth_callback()
|
self._reauth_callback()
|
||||||
|
|
||||||
def _notify_reload_callback(self) -> None:
|
|
||||||
"""Notify reload callback."""
|
|
||||||
if self._reload_callback is not None:
|
|
||||||
self._reload_callback()
|
|
||||||
|
|
||||||
def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None:
|
def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None:
|
||||||
"""Notify update config callback."""
|
"""Notify update config callback."""
|
||||||
if self._update_config_entry is not None:
|
if self._update_config_entry is not None:
|
||||||
@ -597,7 +587,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
|||||||
) == "unrecognized method value : ms.remote.control":
|
) == "unrecognized method value : ms.remote.control":
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
"Your TV seems to be unsupported by SamsungTVWSBridge"
|
"Your TV seems to be unsupported by SamsungTVWSBridge"
|
||||||
" and needs a PIN: '%s'. Reloading",
|
" and needs a PIN: '%s'. Updating config entry",
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
self._notify_update_config_entry(
|
self._notify_update_config_entry(
|
||||||
@ -606,7 +596,6 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
|||||||
CONF_PORT: ENCRYPTED_WEBSOCKET_PORT,
|
CONF_PORT: ENCRYPTED_WEBSOCKET_PORT,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self._notify_reload_callback()
|
|
||||||
|
|
||||||
async def async_power_off(self) -> None:
|
async def async_power_off(self) -> None:
|
||||||
"""Send power off command to remote."""
|
"""Send power off command to remote."""
|
||||||
|
@ -30,6 +30,7 @@ from .const import (
|
|||||||
CONF_MANUFACTURER,
|
CONF_MANUFACTURER,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_SESSION_ID,
|
CONF_SESSION_ID,
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
||||||
DEFAULT_MANUFACTURER,
|
DEFAULT_MANUFACTURER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -46,7 +47,8 @@ from .const import (
|
|||||||
RESULT_SUCCESS,
|
RESULT_SUCCESS,
|
||||||
RESULT_UNKNOWN_HOST,
|
RESULT_UNKNOWN_HOST,
|
||||||
SUCCESSFUL_RESULTS,
|
SUCCESSFUL_RESULTS,
|
||||||
UPNP_SVC_RENDERINGCONTROL,
|
UPNP_SVC_MAIN_TV_AGENT,
|
||||||
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
WEBSOCKET_PORTS,
|
WEBSOCKET_PORTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,7 +60,9 @@ def _strip_uuid(udn: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _entry_is_complete(
|
def _entry_is_complete(
|
||||||
entry: config_entries.ConfigEntry, ssdp_rendering_control_location: str | None
|
entry: config_entries.ConfigEntry,
|
||||||
|
ssdp_rendering_control_location: str | None,
|
||||||
|
ssdp_main_tv_agent_location: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return True if the config entry information is complete.
|
"""Return True if the config entry information is complete.
|
||||||
|
|
||||||
@ -72,6 +76,10 @@ def _entry_is_complete(
|
|||||||
not ssdp_rendering_control_location
|
not ssdp_rendering_control_location
|
||||||
or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
|
or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
|
||||||
)
|
)
|
||||||
|
and (
|
||||||
|
not ssdp_main_tv_agent_location
|
||||||
|
or entry.data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +96,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._udn: str | None = None
|
self._udn: str | None = None
|
||||||
self._upnp_udn: str | None = None
|
self._upnp_udn: str | None = None
|
||||||
self._ssdp_rendering_control_location: str | None = None
|
self._ssdp_rendering_control_location: str | None = None
|
||||||
|
self._ssdp_main_tv_agent_location: str | None = None
|
||||||
self._manufacturer: str | None = None
|
self._manufacturer: str | None = None
|
||||||
self._model: str | None = None
|
self._model: str | None = None
|
||||||
self._connect_result: str | None = None
|
self._connect_result: str | None = None
|
||||||
@ -111,6 +120,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_NAME: self._name,
|
CONF_NAME: self._name,
|
||||||
CONF_PORT: self._bridge.port,
|
CONF_PORT: self._bridge.port,
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location,
|
CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location,
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult:
|
def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult:
|
||||||
@ -141,7 +151,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
await self.async_set_unique_id(self._udn, raise_on_progress=False)
|
await self.async_set_unique_id(self._udn, raise_on_progress=False)
|
||||||
if (
|
if (
|
||||||
entry := self._async_update_existing_matching_entry()
|
entry := self._async_update_existing_matching_entry()
|
||||||
) and _entry_is_complete(entry, self._ssdp_rendering_control_location):
|
) and _entry_is_complete(
|
||||||
|
entry,
|
||||||
|
self._ssdp_rendering_control_location,
|
||||||
|
self._ssdp_main_tv_agent_location,
|
||||||
|
):
|
||||||
raise data_entry_flow.AbortFlow("already_configured")
|
raise data_entry_flow.AbortFlow("already_configured")
|
||||||
# Now that we have updated the config entry, we can raise
|
# Now that we have updated the config entry, we can raise
|
||||||
# if another one is progressing
|
# if another one is progressing
|
||||||
@ -157,7 +171,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
updates[
|
updates[
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION
|
CONF_SSDP_RENDERING_CONTROL_LOCATION
|
||||||
] = self._ssdp_rendering_control_location
|
] = self._ssdp_rendering_control_location
|
||||||
self._abort_if_unique_id_configured(updates=updates)
|
if self._ssdp_main_tv_agent_location:
|
||||||
|
updates[
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION
|
||||||
|
] = self._ssdp_main_tv_agent_location
|
||||||
|
self._abort_if_unique_id_configured(updates=updates, reload_on_update=False)
|
||||||
|
|
||||||
async def _async_create_bridge(self) -> None:
|
async def _async_create_bridge(self) -> None:
|
||||||
"""Create the bridge."""
|
"""Create the bridge."""
|
||||||
@ -342,36 +360,54 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
Returns the existing entry if it was updated.
|
Returns the existing entry if it was updated.
|
||||||
"""
|
"""
|
||||||
entry, is_unique_match = self._async_get_existing_matching_entry()
|
entry, is_unique_match = self._async_get_existing_matching_entry()
|
||||||
if entry:
|
if not entry:
|
||||||
entry_kw_args: dict = {}
|
return None
|
||||||
if self.unique_id and (
|
entry_kw_args: dict = {}
|
||||||
entry.unique_id is None
|
if (
|
||||||
or (is_unique_match and self.unique_id != entry.unique_id)
|
self.unique_id
|
||||||
):
|
and entry.unique_id is None
|
||||||
entry_kw_args["unique_id"] = self.unique_id
|
or (is_unique_match and self.unique_id != entry.unique_id)
|
||||||
data = entry.data
|
):
|
||||||
update_ssdp_rendering_control_location = (
|
entry_kw_args["unique_id"] = self.unique_id
|
||||||
self._ssdp_rendering_control_location
|
data: dict[str, Any] = dict(entry.data)
|
||||||
and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
|
update_ssdp_rendering_control_location = (
|
||||||
!= self._ssdp_rendering_control_location
|
self._ssdp_rendering_control_location
|
||||||
|
and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
|
||||||
|
!= self._ssdp_rendering_control_location
|
||||||
|
)
|
||||||
|
update_ssdp_main_tv_agent_location = (
|
||||||
|
self._ssdp_main_tv_agent_location
|
||||||
|
and data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION)
|
||||||
|
!= self._ssdp_main_tv_agent_location
|
||||||
|
)
|
||||||
|
update_mac = self._mac and not data.get(CONF_MAC)
|
||||||
|
if (
|
||||||
|
update_ssdp_rendering_control_location
|
||||||
|
or update_ssdp_main_tv_agent_location
|
||||||
|
or update_mac
|
||||||
|
):
|
||||||
|
if update_ssdp_rendering_control_location:
|
||||||
|
data[
|
||||||
|
CONF_SSDP_RENDERING_CONTROL_LOCATION
|
||||||
|
] = self._ssdp_rendering_control_location
|
||||||
|
if update_ssdp_main_tv_agent_location:
|
||||||
|
data[
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION
|
||||||
|
] = self._ssdp_main_tv_agent_location
|
||||||
|
if update_mac:
|
||||||
|
data[CONF_MAC] = self._mac
|
||||||
|
entry_kw_args["data"] = data
|
||||||
|
if not entry_kw_args:
|
||||||
|
return None
|
||||||
|
LOGGER.debug("Updating existing config entry with %s", entry_kw_args)
|
||||||
|
self.hass.config_entries.async_update_entry(entry, **entry_kw_args)
|
||||||
|
if entry.state != config_entries.ConfigEntryState.LOADED:
|
||||||
|
# If its loaded it already has a reload listener in place
|
||||||
|
# and we do not want to trigger multiple reloads
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(entry.entry_id)
|
||||||
)
|
)
|
||||||
update_mac = self._mac and not data.get(CONF_MAC)
|
return entry
|
||||||
if update_ssdp_rendering_control_location or update_mac:
|
|
||||||
entry_kw_args["data"] = {**entry.data}
|
|
||||||
if update_ssdp_rendering_control_location:
|
|
||||||
entry_kw_args["data"][
|
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION
|
|
||||||
] = self._ssdp_rendering_control_location
|
|
||||||
if update_mac:
|
|
||||||
entry_kw_args["data"][CONF_MAC] = self._mac
|
|
||||||
if entry_kw_args:
|
|
||||||
LOGGER.debug("Updating existing config entry with %s", entry_kw_args)
|
|
||||||
self.hass.config_entries.async_update_entry(entry, **entry_kw_args)
|
|
||||||
self.hass.async_create_task(
|
|
||||||
self.hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
)
|
|
||||||
return entry
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _async_start_discovery_with_mac_address(self) -> None:
|
async def _async_start_discovery_with_mac_address(self) -> None:
|
||||||
"""Start discovery."""
|
"""Start discovery."""
|
||||||
@ -402,10 +438,17 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a flow initialized by ssdp discovery."""
|
"""Handle a flow initialized by ssdp discovery."""
|
||||||
LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)
|
LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)
|
||||||
model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or ""
|
model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or ""
|
||||||
if discovery_info.ssdp_st == UPNP_SVC_RENDERINGCONTROL:
|
if discovery_info.ssdp_st == UPNP_SVC_RENDERING_CONTROL:
|
||||||
self._ssdp_rendering_control_location = discovery_info.ssdp_location
|
self._ssdp_rendering_control_location = discovery_info.ssdp_location
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Set SSDP location to: %s", self._ssdp_rendering_control_location
|
"Set SSDP RenderingControl location to: %s",
|
||||||
|
self._ssdp_rendering_control_location,
|
||||||
|
)
|
||||||
|
elif discovery_info.ssdp_st == UPNP_SVC_MAIN_TV_AGENT:
|
||||||
|
self._ssdp_main_tv_agent_location = discovery_info.ssdp_location
|
||||||
|
LOGGER.debug(
|
||||||
|
"Set SSDP MainTvAgent location to: %s",
|
||||||
|
self._ssdp_main_tv_agent_location,
|
||||||
)
|
)
|
||||||
self._udn = self._upnp_udn = _strip_uuid(
|
self._udn = self._upnp_udn = _strip_uuid(
|
||||||
discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
|
discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
|
||||||
|
@ -16,6 +16,7 @@ CONF_DESCRIPTION = "description"
|
|||||||
CONF_MANUFACTURER = "manufacturer"
|
CONF_MANUFACTURER = "manufacturer"
|
||||||
CONF_MODEL = "model"
|
CONF_MODEL = "model"
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location"
|
CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location"
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION = "ssdp_main_tv_agent_location"
|
||||||
CONF_ON_ACTION = "turn_on_action"
|
CONF_ON_ACTION = "turn_on_action"
|
||||||
CONF_SESSION_ID = "session_id"
|
CONF_SESSION_ID = "session_id"
|
||||||
|
|
||||||
@ -41,4 +42,8 @@ WEBSOCKET_PORTS = (WEBSOCKET_SSL_PORT, WEBSOCKET_NO_SSL_PORT)
|
|||||||
|
|
||||||
SUCCESSFUL_RESULTS = {RESULT_AUTH_MISSING, RESULT_SUCCESS}
|
SUCCESSFUL_RESULTS = {RESULT_AUTH_MISSING, RESULT_SUCCESS}
|
||||||
|
|
||||||
UPNP_SVC_RENDERINGCONTROL = "urn:schemas-upnp-org:service:RenderingControl:1"
|
UPNP_SVC_RENDERING_CONTROL = "urn:schemas-upnp-org:service:RenderingControl:1"
|
||||||
|
UPNP_SVC_MAIN_TV_AGENT = "urn:samsung.com:service:MainTVAgent2:1"
|
||||||
|
|
||||||
|
# Time to wait before reloading entry upon device config change
|
||||||
|
ENTRY_RELOAD_COOLDOWN = 5
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
{
|
{
|
||||||
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
|
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"st": "urn:samsung.com:service:MainTVAgent2:1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"manufacturer": "Samsung",
|
"manufacturer": "Samsung",
|
||||||
"st": "urn:schemas-upnp-org:service:RenderingControl:1"
|
"st": "urn:schemas-upnp-org:service:RenderingControl:1"
|
||||||
@ -25,6 +28,7 @@
|
|||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}}
|
{"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}}
|
||||||
],
|
],
|
||||||
|
"dependencies": ["ssdp"],
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
{"registered_devices": true},
|
{"registered_devices": true},
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,7 @@ from .const import (
|
|||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
UPNP_SVC_RENDERINGCONTROL,
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
)
|
)
|
||||||
|
|
||||||
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
||||||
@ -256,13 +256,13 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
LOGGER.info("Upnp services are not available on %s", self._host)
|
LOGGER.info("Upnp services are not available on %s", self._host)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if service := self._upnp_device.services.get(UPNP_SVC_RENDERINGCONTROL):
|
if service := self._upnp_device.services.get(UPNP_SVC_RENDERING_CONTROL):
|
||||||
return service
|
return service
|
||||||
|
|
||||||
if log:
|
if log:
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Upnp service %s is not available on %s",
|
"Upnp service %s is not available on %s",
|
||||||
UPNP_SVC_RENDERINGCONTROL,
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
self._host,
|
self._host,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
@ -226,6 +226,9 @@ SSDP = {
|
|||||||
{
|
{
|
||||||
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
|
"st": "urn:samsung.com:device:RemoteControlReceiver:1"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"st": "urn:samsung.com:service:MainTVAgent2:1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"manufacturer": "Samsung",
|
"manufacturer": "Samsung",
|
||||||
"st": "urn:schemas-upnp-org:service:RenderingControl:1"
|
"st": "urn:schemas-upnp-org:service:RenderingControl:1"
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
"""Tests for the samsungtv component."""
|
"""Tests for the samsungtv component."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from async_upnp_client.client import UpnpAction, UpnpService
|
from async_upnp_client.client import UpnpAction, UpnpService
|
||||||
|
|
||||||
from homeassistant.components.samsungtv.const import DOMAIN
|
from homeassistant.components.samsungtv.const import DOMAIN, ENTRY_RELOAD_COOLDOWN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def async_wait_config_entry_reload(hass: HomeAssistant) -> None:
|
||||||
|
"""Wait for the config entry to reload."""
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry:
|
async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry:
|
||||||
|
@ -23,6 +23,22 @@ import homeassistant.util.dt as dt_util
|
|||||||
from .const import SAMPLE_DEVICE_INFO_WIFI
|
from .const import SAMPLE_DEVICE_INFO_WIFI
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def silent_ssdp_scanner(hass):
|
||||||
|
"""Start SSDP component and get Scanner, prevent actual SSDP traffic."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
||||||
|
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
||||||
|
"homeassistant.components.ssdp.Scanner.async_scan"
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def samsungtv_mock_get_source_ip(mock_get_source_ip):
|
||||||
|
"""Mock network util's async_get_source_ip."""
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def fake_host_fixture() -> None:
|
def fake_host_fixture() -> None:
|
||||||
"""Patch gethostbyname."""
|
"""Patch gethostbyname."""
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
"""Constants for the samsungtv tests."""
|
"""Constants for the samsungtv tests."""
|
||||||
from samsungtvws.event import ED_INSTALLED_APP_EVENT
|
from samsungtvws.event import ED_INSTALLED_APP_EVENT
|
||||||
|
|
||||||
from homeassistant.components.samsungtv.const import CONF_SESSION_ID
|
from homeassistant.components import ssdp
|
||||||
|
from homeassistant.components.samsungtv.const import (
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_SESSION_ID,
|
||||||
|
METHOD_WEBSOCKET,
|
||||||
|
)
|
||||||
|
from homeassistant.components.ssdp import (
|
||||||
|
ATTR_UPNP_FRIENDLY_NAME,
|
||||||
|
ATTR_UPNP_MANUFACTURER,
|
||||||
|
ATTR_UPNP_MODEL_NAME,
|
||||||
|
ATTR_UPNP_UDN,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
@ -25,6 +36,36 @@ MOCK_ENTRYDATA_ENCRYPTED_WS = {
|
|||||||
CONF_TOKEN: "037739871315caef138547b03e348b72",
|
CONF_TOKEN: "037739871315caef138547b03e348b72",
|
||||||
CONF_SESSION_ID: "2",
|
CONF_SESSION_ID: "2",
|
||||||
}
|
}
|
||||||
|
MOCK_ENTRYDATA_WS = {
|
||||||
|
CONF_HOST: "fake_host",
|
||||||
|
CONF_METHOD: METHOD_WEBSOCKET,
|
||||||
|
CONF_PORT: 8002,
|
||||||
|
CONF_MODEL: "any",
|
||||||
|
CONF_NAME: "any",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo(
|
||||||
|
ssdp_usn="mock_usn",
|
||||||
|
ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1",
|
||||||
|
ssdp_location="https://fake_host:12345/test",
|
||||||
|
upnp={
|
||||||
|
ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name",
|
||||||
|
ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer",
|
||||||
|
ATTR_UPNP_MODEL_NAME: "fake_model",
|
||||||
|
ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = ssdp.SsdpServiceInfo(
|
||||||
|
ssdp_usn="mock_usn",
|
||||||
|
ssdp_st="urn:samsung.com:service:MainTVAgent2:1",
|
||||||
|
ssdp_location="https://fake_host:12345/tv_agent",
|
||||||
|
upnp={
|
||||||
|
ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name",
|
||||||
|
ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer",
|
||||||
|
ATTR_UPNP_MODEL_NAME: "fake_model",
|
||||||
|
ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
SAMPLE_DEVICE_INFO_WIFI = {
|
SAMPLE_DEVICE_INFO_WIFI = {
|
||||||
"id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
|
"id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||||
|
@ -24,6 +24,7 @@ from homeassistant.components.samsungtv.const import (
|
|||||||
CONF_MANUFACTURER,
|
CONF_MANUFACTURER,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_SESSION_ID,
|
CONF_SESSION_ID,
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
|
||||||
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
||||||
DEFAULT_MANUFACTURER,
|
DEFAULT_MANUFACTURER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -66,6 +67,9 @@ from . import setup_samsungtv_entry
|
|||||||
from .const import (
|
from .const import (
|
||||||
MOCK_CONFIG_ENCRYPTED_WS,
|
MOCK_CONFIG_ENCRYPTED_WS,
|
||||||
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
||||||
|
MOCK_ENTRYDATA_WS,
|
||||||
|
MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||||
|
MOCK_SSDP_DATA_RENDERING_CONTROL_ST,
|
||||||
SAMPLE_DEVICE_INFO_FRAME,
|
SAMPLE_DEVICE_INFO_FRAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,17 +104,6 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1",
|
|
||||||
ssdp_location="https://fake_host:12345/test",
|
|
||||||
upnp={
|
|
||||||
ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name",
|
|
||||||
ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer",
|
|
||||||
ATTR_UPNP_MODEL_NAME: "fake_model",
|
|
||||||
ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo(
|
MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo(
|
||||||
ssdp_usn="mock_usn",
|
ssdp_usn="mock_usn",
|
||||||
ssdp_st="mock_st",
|
ssdp_st="mock_st",
|
||||||
@ -164,13 +157,6 @@ MOCK_LEGACY_ENTRY = {
|
|||||||
CONF_METHOD: "legacy",
|
CONF_METHOD: "legacy",
|
||||||
CONF_PORT: None,
|
CONF_PORT: None,
|
||||||
}
|
}
|
||||||
MOCK_WS_ENTRY = {
|
|
||||||
CONF_HOST: "fake_host",
|
|
||||||
CONF_METHOD: METHOD_WEBSOCKET,
|
|
||||||
CONF_PORT: 8002,
|
|
||||||
CONF_MODEL: "any",
|
|
||||||
CONF_NAME: "any",
|
|
||||||
}
|
|
||||||
MOCK_DEVICE_INFO = {
|
MOCK_DEVICE_INFO = {
|
||||||
"device": {
|
"device": {
|
||||||
"type": "Samsung SmartTV",
|
"type": "Samsung SmartTV",
|
||||||
@ -643,6 +629,36 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location(
|
|||||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||||
|
async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_location(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test starting a flow from ssdp for a supported device populates the mac."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_SSDP},
|
||||||
|
data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input="whatever"
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "Living Room (82GXARRS)"
|
||||||
|
assert result["data"][CONF_HOST] == "fake_host"
|
||||||
|
assert result["data"][CONF_NAME] == "Living Room"
|
||||||
|
assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii"
|
||||||
|
assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer"
|
||||||
|
assert result["data"][CONF_MODEL] == "82GXARRS"
|
||||||
|
assert (
|
||||||
|
result["data"][CONF_SSDP_MAIN_TV_AGENT_LOCATION]
|
||||||
|
== "https://fake_host:12345/tv_agent"
|
||||||
|
)
|
||||||
|
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only")
|
@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only")
|
||||||
async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location(
|
async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -1504,6 +1520,52 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd
|
|||||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||||
|
async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
**MOCK_OLD_ENTRY,
|
||||||
|
CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test",
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION: "https://1.2.3.4:555/test",
|
||||||
|
},
|
||||||
|
unique_id=None,
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.async_setup",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.samsungtv.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_SSDP},
|
||||||
|
data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii"
|
||||||
|
# Main TV Agent ST, ssdp location should change
|
||||||
|
assert (
|
||||||
|
entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION]
|
||||||
|
== "https://fake_host:12345/tv_agent"
|
||||||
|
)
|
||||||
|
# Rendering control should not be affected
|
||||||
|
assert (
|
||||||
|
entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://1.2.3.4:555/test"
|
||||||
|
)
|
||||||
|
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||||
async def test_update_ssdp_location_rendering_st_updated_from_ssdp(
|
async def test_update_ssdp_location_rendering_st_updated_from_ssdp(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -1542,6 +1604,44 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp(
|
|||||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||||
|
async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test with outdated ssdp_location with the correct st added via ssdp."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"},
|
||||||
|
unique_id="be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.async_setup",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.samsungtv.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_SSDP},
|
||||||
|
data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii"
|
||||||
|
# Correct ST for MainTV, ssdp location should be added
|
||||||
|
assert (
|
||||||
|
entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION]
|
||||||
|
== "https://fake_host:12345/tv_agent"
|
||||||
|
)
|
||||||
|
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||||
async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
|
async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -1744,7 +1844,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None:
|
|||||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||||
async def test_form_reauth_websocket(hass: HomeAssistant) -> None:
|
async def test_form_reauth_websocket(hass: HomeAssistant) -> None:
|
||||||
"""Test reauthenticate websocket."""
|
"""Test reauthenticate websocket."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
assert entry.state == config_entries.ConfigEntryState.NOT_LOADED
|
assert entry.state == config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
@ -1771,7 +1871,7 @@ async def test_form_reauth_websocket_cannot_connect(
|
|||||||
hass: HomeAssistant, remotews: Mock
|
hass: HomeAssistant, remotews: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauthenticate websocket when we cannot connect on the first attempt."""
|
"""Test reauthenticate websocket when we cannot connect on the first attempt."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -1803,7 +1903,7 @@ async def test_form_reauth_websocket_cannot_connect(
|
|||||||
|
|
||||||
async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None:
|
async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None:
|
||||||
"""Test reauthenticate websocket when the device is not supported."""
|
"""Test reauthenticate websocket when the device is not supported."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -1972,7 +2072,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp(
|
|||||||
"""Test that DHCP updates the wrong udn from ssdp via mac match."""
|
"""Test that DHCP updates the wrong udn from ssdp via mac match."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"},
|
data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ww:ii:ff:ii"},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||||
)
|
)
|
||||||
@ -2006,7 +2106,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp(
|
|||||||
"""Test that DHCP does not update the wrong udn from ssdp via host match."""
|
"""Test that DHCP does not update the wrong udn from ssdp via host match."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ss:ss:dd:pp"},
|
data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ss:ss:dd:pp"},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||||
)
|
)
|
||||||
|
@ -7,8 +7,12 @@ import pytest
|
|||||||
from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON
|
from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON
|
||||||
from homeassistant.components.samsungtv.const import (
|
from homeassistant.components.samsungtv.const import (
|
||||||
CONF_ON_ACTION,
|
CONF_ON_ACTION,
|
||||||
|
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
|
||||||
|
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
||||||
DOMAIN as SAMSUNGTV_DOMAIN,
|
DOMAIN as SAMSUNGTV_DOMAIN,
|
||||||
METHOD_WEBSOCKET,
|
METHOD_WEBSOCKET,
|
||||||
|
UPNP_SVC_MAIN_TV_AGENT,
|
||||||
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
)
|
)
|
||||||
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
|
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
@ -24,6 +28,14 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
MOCK_ENTRYDATA_WS,
|
||||||
|
MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||||
|
MOCK_SSDP_DATA_RENDERING_CONTROL_ST,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
ENTITY_ID = f"{DOMAIN}.fake_name"
|
ENTITY_ID = f"{DOMAIN}.fake_name"
|
||||||
MOCK_CONFIG = {
|
MOCK_CONFIG = {
|
||||||
SAMSUNGTV_DOMAIN: [
|
SAMSUNGTV_DOMAIN: [
|
||||||
@ -156,3 +168,35 @@ async def test_setup_h_j_model(
|
|||||||
state = hass.states.get(ENTITY_ID)
|
state = hass.states.get(ENTITY_ID)
|
||||||
assert state
|
assert state
|
||||||
assert "H and J series use an encrypted protocol" in caplog.text
|
assert "H and J series use an encrypted protocol" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("remote", "remotews", "remoteencws_failing")
|
||||||
|
async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setting up the entry fetches data from ssdp cache."""
|
||||||
|
entry = MockConfigEntry(domain="samsungtv", data=MOCK_ENTRYDATA_WS)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async def _mock_async_get_discovery_info_by_st(hass: HomeAssistant, mock_st: str):
|
||||||
|
if mock_st == UPNP_SVC_RENDERING_CONTROL:
|
||||||
|
return [MOCK_SSDP_DATA_RENDERING_CONTROL_ST]
|
||||||
|
if mock_st == UPNP_SVC_MAIN_TV_AGENT:
|
||||||
|
return [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST]
|
||||||
|
raise ValueError(f"Unknown st {mock_st}")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.samsungtv.ssdp.async_get_discovery_info_by_st",
|
||||||
|
_mock_async_get_discovery_info_by_st,
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("media_player.any")
|
||||||
|
assert (
|
||||||
|
entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION]
|
||||||
|
== "https://fake_host:12345/tv_agent"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION]
|
||||||
|
== "https://fake_host:12345/test"
|
||||||
|
)
|
||||||
|
@ -44,7 +44,7 @@ from homeassistant.components.samsungtv.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.samsungtv.media_player import (
|
from homeassistant.components.samsungtv.media_player import (
|
||||||
SUPPORT_SAMSUNGTV,
|
SUPPORT_SAMSUNGTV,
|
||||||
UPNP_SVC_RENDERINGCONTROL,
|
UPNP_SVC_RENDERING_CONTROL,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
@ -80,7 +80,11 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
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
|
||||||
|
|
||||||
from . import setup_samsungtv_entry, upnp_get_action_mock
|
from . import (
|
||||||
|
async_wait_config_entry_reload,
|
||||||
|
setup_samsungtv_entry,
|
||||||
|
upnp_get_action_mock,
|
||||||
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
||||||
SAMPLE_DEVICE_INFO_FRAME,
|
SAMPLE_DEVICE_INFO_FRAME,
|
||||||
@ -1301,8 +1305,8 @@ async def test_websocket_unsupported_remote_control(
|
|||||||
"'unrecognized method value : ms.remote.control'" in caplog.text
|
"'unrecognized method value : ms.remote.control'" in caplog.text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await async_wait_config_entry_reload(hass)
|
||||||
# ensure reauth triggered, and method/port updated
|
# ensure reauth triggered, and method/port updated
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert [
|
assert [
|
||||||
flow
|
flow
|
||||||
for flow in hass.config_entries.flow.async_progress()
|
for flow in hass.config_entries.flow.async_progress()
|
||||||
@ -1320,12 +1324,12 @@ async def test_volume_control_upnp(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test for Upnp volume control."""
|
"""Test for Upnp volume control."""
|
||||||
upnp_get_volume = upnp_get_action_mock(
|
upnp_get_volume = upnp_get_action_mock(
|
||||||
upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetVolume"
|
upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetVolume"
|
||||||
)
|
)
|
||||||
upnp_get_volume.async_call.return_value = {"CurrentVolume": 44}
|
upnp_get_volume.async_call.return_value = {"CurrentVolume": 44}
|
||||||
|
|
||||||
upnp_get_mute = upnp_get_action_mock(
|
upnp_get_mute = upnp_get_action_mock(
|
||||||
upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetMute"
|
upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetMute"
|
||||||
)
|
)
|
||||||
upnp_get_mute.async_call.return_value = {"CurrentMute": False}
|
upnp_get_mute.async_call.return_value = {"CurrentMute": False}
|
||||||
|
|
||||||
@ -1335,7 +1339,7 @@ async def test_volume_control_upnp(
|
|||||||
|
|
||||||
# Upnp action succeeds
|
# Upnp action succeeds
|
||||||
upnp_set_volume = upnp_get_action_mock(
|
upnp_set_volume = upnp_get_action_mock(
|
||||||
upnp_device, UPNP_SVC_RENDERINGCONTROL, "SetVolume"
|
upnp_device, UPNP_SVC_RENDERING_CONTROL, "SetVolume"
|
||||||
)
|
)
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -1389,4 +1393,4 @@ async def test_upnp_missing_service(
|
|||||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
assert f"Upnp service {UPNP_SVC_RENDERINGCONTROL} is not available" in caplog.text
|
assert f"Upnp service {UPNP_SVC_RENDERING_CONTROL} is not available" in caplog.text
|
||||||
|
Loading…
x
Reference in New Issue
Block a user