mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Use DmrDevice to communicate with SamsungTV (#68777)
Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
parent
d0e5e51863
commit
8eb2e131e5
@ -2,15 +2,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Coroutine, Sequence
|
||||
import contextlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from async_upnp_client.aiohttp import AiohttpSessionRequester
|
||||
from async_upnp_client.client import UpnpDevice, UpnpService
|
||||
from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester
|
||||
from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable
|
||||
from async_upnp_client.client_factory import UpnpFactory
|
||||
from async_upnp_client.exceptions import UpnpActionResponseError, UpnpConnectionError
|
||||
from async_upnp_client.exceptions import (
|
||||
UpnpActionResponseError,
|
||||
UpnpConnectionError,
|
||||
UpnpError,
|
||||
UpnpResponseError,
|
||||
)
|
||||
from async_upnp_client.profiles.dlna import DmrDevice
|
||||
from async_upnp_client.utils import async_get_local_ip
|
||||
import voluptuous as vol
|
||||
from wakeonlan import send_magic_packet
|
||||
|
||||
@ -35,7 +42,7 @@ from homeassistant.components.media_player.const import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_component
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -54,7 +61,6 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
UPNP_SVC_RENDERING_CONTROL,
|
||||
)
|
||||
|
||||
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
||||
@ -114,7 +120,7 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
self._config_entry = config_entry
|
||||
self._host: str | None = config_entry.data[CONF_HOST]
|
||||
self._mac: str | None = config_entry.data.get(CONF_MAC)
|
||||
self._ssdp_rendering_control_location = config_entry.data.get(
|
||||
self._ssdp_rendering_control_location: str | None = config_entry.data.get(
|
||||
CONF_SSDP_RENDERING_CONTROL_LOCATION
|
||||
)
|
||||
self._on_script = on_script
|
||||
@ -157,7 +163,8 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
self._bridge.register_reauth_callback(self.access_denied)
|
||||
self._bridge.register_app_list_callback(self._app_list_callback)
|
||||
|
||||
self._upnp_device: UpnpDevice | None = None
|
||||
self._dmr_device: DmrDevice | None = None
|
||||
self._upnp_server: AiohttpNotifyServer | None = None
|
||||
|
||||
def _update_sources(self) -> None:
|
||||
self._attr_source_list = list(SOURCES)
|
||||
@ -185,6 +192,10 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle removal."""
|
||||
await self._async_shutdown_dmr()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update state of device."""
|
||||
if self._auth_failed or self.hass.is_stopping:
|
||||
@ -204,24 +215,22 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
if not self._app_list_event.is_set():
|
||||
startup_tasks.append(self._async_startup_app_list())
|
||||
|
||||
if not self._upnp_device and self._ssdp_rendering_control_location:
|
||||
startup_tasks.append(self._async_startup_upnp())
|
||||
if not self._dmr_device and self._ssdp_rendering_control_location:
|
||||
startup_tasks.append(self._async_startup_dmr())
|
||||
|
||||
if startup_tasks:
|
||||
await asyncio.gather(*startup_tasks)
|
||||
|
||||
if not (service := self._get_upnp_service()):
|
||||
self._update_from_upnp()
|
||||
|
||||
@callback
|
||||
def _update_from_upnp(self) -> None:
|
||||
if (dmr_device := self._dmr_device) is None:
|
||||
return
|
||||
|
||||
get_volume, get_mute = await asyncio.gather(
|
||||
service.action("GetVolume").async_call(InstanceID=0, Channel="Master"),
|
||||
service.action("GetMute").async_call(InstanceID=0, Channel="Master"),
|
||||
)
|
||||
LOGGER.debug("Upnp GetVolume on %s: %s", self._host, get_volume)
|
||||
if (volume_level := get_volume.get("CurrentVolume")) is not None:
|
||||
self._attr_volume_level = volume_level / 100
|
||||
LOGGER.debug("Upnp GetMute on %s: %s", self._host, get_mute)
|
||||
if (is_muted := get_mute.get("CurrentMute")) is not None:
|
||||
if (volume_level := dmr_device.volume_level) is not None:
|
||||
self._attr_volume_level = volume_level
|
||||
if (is_muted := dmr_device.is_volume_muted) is not None:
|
||||
self._attr_is_volume_muted = is_muted
|
||||
|
||||
async def _async_startup_app_list(self) -> None:
|
||||
@ -239,33 +248,66 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
"Failed to load app list from %s: %s", self._host, err.__repr__()
|
||||
)
|
||||
|
||||
async def _async_startup_upnp(self) -> None:
|
||||
async def _async_startup_dmr(self) -> None:
|
||||
assert self._ssdp_rendering_control_location is not None
|
||||
if self._upnp_device is None:
|
||||
if self._dmr_device is None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
upnp_requester = AiohttpSessionRequester(session)
|
||||
upnp_factory = UpnpFactory(upnp_requester)
|
||||
upnp_device: UpnpDevice | None = None
|
||||
with contextlib.suppress(UpnpConnectionError):
|
||||
self._upnp_device = await upnp_factory.async_create_device(
|
||||
upnp_device = await upnp_factory.async_create_device(
|
||||
self._ssdp_rendering_control_location
|
||||
)
|
||||
|
||||
def _get_upnp_service(self, log: bool = False) -> UpnpService | None:
|
||||
if self._upnp_device is None:
|
||||
if log:
|
||||
LOGGER.info("Upnp services are not available on %s", self._host)
|
||||
return None
|
||||
|
||||
if service := self._upnp_device.services.get(UPNP_SVC_RENDERING_CONTROL):
|
||||
return service
|
||||
|
||||
if log:
|
||||
LOGGER.info(
|
||||
"Upnp service %s is not available on %s",
|
||||
UPNP_SVC_RENDERING_CONTROL,
|
||||
self._host,
|
||||
if not upnp_device:
|
||||
return
|
||||
_, event_ip = await async_get_local_ip(
|
||||
self._ssdp_rendering_control_location, self.hass.loop
|
||||
)
|
||||
return None
|
||||
source = (event_ip or "0.0.0.0", 0)
|
||||
self._upnp_server = AiohttpNotifyServer(
|
||||
requester=upnp_requester,
|
||||
source=source,
|
||||
callback_url=None,
|
||||
loop=self.hass.loop,
|
||||
)
|
||||
await self._upnp_server.async_start_server()
|
||||
self._dmr_device = DmrDevice(upnp_device, self._upnp_server.event_handler)
|
||||
|
||||
try:
|
||||
self._dmr_device.on_event = self._on_upnp_event
|
||||
await self._dmr_device.async_subscribe_services(auto_resubscribe=True)
|
||||
except UpnpResponseError as err:
|
||||
# Device rejected subscription request. This is OK, variables
|
||||
# will be polled instead.
|
||||
LOGGER.debug("Device rejected subscription: %r", err)
|
||||
except UpnpError as err:
|
||||
# Don't leave the device half-constructed
|
||||
self._dmr_device.on_event = None
|
||||
self._dmr_device = None
|
||||
await self._upnp_server.async_stop_server()
|
||||
self._upnp_server = None
|
||||
LOGGER.debug("Error while subscribing during device connect: %r", err)
|
||||
raise
|
||||
|
||||
async def _async_shutdown_dmr(self) -> None:
|
||||
"""Handle removal."""
|
||||
if (dmr_device := self._dmr_device) is not None:
|
||||
self._dmr_device = None
|
||||
dmr_device.on_event = None
|
||||
await dmr_device.async_unsubscribe_services()
|
||||
|
||||
if (upnp_server := self._upnp_server) is not None:
|
||||
self._upnp_server = None
|
||||
await upnp_server.async_stop_server()
|
||||
|
||||
def _on_upnp_event(
|
||||
self, service: UpnpService, state_variables: Sequence[UpnpStateVariable]
|
||||
) -> None:
|
||||
"""State variable(s) changed, let home-assistant know."""
|
||||
self._update_from_upnp()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_launch_app(self, app_id: str) -> None:
|
||||
"""Send launch_app to the tv."""
|
||||
@ -308,12 +350,11 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level on the media player."""
|
||||
if not (service := self._get_upnp_service(log=True)):
|
||||
if (dmr_device := self._dmr_device) is None:
|
||||
LOGGER.info("Upnp services are not available on %s", self._host)
|
||||
return
|
||||
try:
|
||||
await service.action("SetVolume").async_call(
|
||||
InstanceID=0, Channel="Master", DesiredVolume=int(volume * 100)
|
||||
)
|
||||
await dmr_device.async_set_volume_level(volume)
|
||||
except UpnpActionResponseError as err:
|
||||
LOGGER.warning(
|
||||
"Unable to set volume level on %s: %s", self._host, err.__repr__()
|
||||
|
@ -2,9 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from async_upnp_client.client import UpnpAction, UpnpService
|
||||
|
||||
from homeassistant.components.samsungtv.const import DOMAIN, ENTRY_RELOAD_COOLDOWN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -36,24 +33,3 @@ async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> Config
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def upnp_get_action_mock(device: Mock, service_type: str, action: str) -> Mock:
|
||||
"""Get or Add UpnpService/UpnpAction to UpnpDevice mock."""
|
||||
upnp_service: Mock | None
|
||||
if (upnp_service := device.services.get(service_type)) is None:
|
||||
upnp_service = Mock(UpnpService)
|
||||
upnp_service.actions = {}
|
||||
|
||||
def _get_action(action: str):
|
||||
return upnp_service.actions.get(action)
|
||||
|
||||
upnp_service.action.side_effect = _get_action
|
||||
device.services[service_type] = upnp_service
|
||||
|
||||
upnp_action: Mock | None
|
||||
if (upnp_action := upnp_service.actions.get(action)) is None:
|
||||
upnp_action = Mock(UpnpAction)
|
||||
upnp_service.actions[action] = upnp_action
|
||||
|
||||
return upnp_action
|
||||
|
@ -3,10 +3,12 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime
|
||||
from socket import AddressFamily
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from async_upnp_client.client import UpnpDevice
|
||||
from async_upnp_client.event_handler import UpnpEventHandler
|
||||
from async_upnp_client.exceptions import UpnpConnectionError
|
||||
import pytest
|
||||
from samsungctl import Remote
|
||||
@ -39,6 +41,16 @@ def samsungtv_mock_get_source_ip(mock_get_source_ip):
|
||||
"""Mock network util's async_get_source_ip."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def samsungtv_mock_async_get_local_ip():
|
||||
"""Mock upnp util's async_get_local_ip."""
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.media_player.async_get_local_ip",
|
||||
return_value=(AddressFamily.AF_INET, "10.10.10.10"),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_host_fixture() -> None:
|
||||
"""Patch gethostbyname."""
|
||||
@ -78,6 +90,38 @@ async def upnp_device_fixture(upnp_factory: Mock) -> Mock:
|
||||
yield upnp_device
|
||||
|
||||
|
||||
@pytest.fixture(name="dmr_device")
|
||||
async def dmr_device_fixture(upnp_device: Mock) -> Mock:
|
||||
"""Patch async_upnp_client."""
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.media_player.DmrDevice",
|
||||
autospec=True,
|
||||
) as dmr_device_class:
|
||||
dmr_device: Mock = dmr_device_class.return_value
|
||||
dmr_device.volume_level = 0.44
|
||||
dmr_device.is_volume_muted = False
|
||||
dmr_device.on_event = None
|
||||
|
||||
def _raise_event(service, state_variables):
|
||||
if dmr_device.on_event:
|
||||
dmr_device.on_event(service, state_variables)
|
||||
|
||||
dmr_device.raise_event = _raise_event
|
||||
yield dmr_device
|
||||
|
||||
|
||||
@pytest.fixture(name="upnp_notify_server")
|
||||
async def upnp_notify_server_fixture(upnp_factory: Mock) -> Mock:
|
||||
"""Patch async_upnp_client."""
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.media_player.AiohttpNotifyServer",
|
||||
autospec=True,
|
||||
) as notify_server_class:
|
||||
notify_server: Mock = notify_server_class.return_value
|
||||
notify_server.event_handler = Mock(UpnpEventHandler)
|
||||
yield notify_server
|
||||
|
||||
|
||||
@pytest.fixture(name="remote")
|
||||
def remote_fixture() -> Mock:
|
||||
"""Patch the samsungctl Remote."""
|
||||
|
@ -4,7 +4,11 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch
|
||||
|
||||
from async_upnp_client.exceptions import UpnpActionResponseError
|
||||
from async_upnp_client.exceptions import (
|
||||
UpnpActionResponseError,
|
||||
UpnpError,
|
||||
UpnpResponseError,
|
||||
)
|
||||
import pytest
|
||||
from samsungctl import exceptions
|
||||
from samsungtvws.async_remote import SamsungTVWSAsyncRemote
|
||||
@ -42,10 +46,7 @@ from homeassistant.components.samsungtv.const import (
|
||||
METHOD_WEBSOCKET,
|
||||
TIMEOUT_WEBSOCKET,
|
||||
)
|
||||
from homeassistant.components.samsungtv.media_player import (
|
||||
SUPPORT_SAMSUNGTV,
|
||||
UPNP_SVC_RENDERING_CONTROL,
|
||||
)
|
||||
from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
@ -80,11 +81,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import (
|
||||
async_wait_config_entry_reload,
|
||||
setup_samsungtv_entry,
|
||||
upnp_get_action_mock,
|
||||
)
|
||||
from . import async_wait_config_entry_reload, setup_samsungtv_entry
|
||||
from .const import (
|
||||
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
||||
SAMPLE_DEVICE_INFO_FRAME,
|
||||
@ -1329,39 +1326,30 @@ async def test_websocket_unsupported_remote_control(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server")
|
||||
async def test_volume_control_upnp(
|
||||
hass: HomeAssistant, upnp_device: Mock, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant, dmr_device: Mock, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test for Upnp volume control."""
|
||||
upnp_get_volume = upnp_get_action_mock(
|
||||
upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetVolume"
|
||||
)
|
||||
upnp_get_volume.async_call.return_value = {"CurrentVolume": 44}
|
||||
|
||||
upnp_get_mute = upnp_get_action_mock(
|
||||
upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetMute"
|
||||
)
|
||||
upnp_get_mute.async_call.return_value = {"CurrentMute": False}
|
||||
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
|
||||
upnp_get_volume.async_call.assert_called_once()
|
||||
upnp_get_mute.async_call.assert_called_once()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.44
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is False
|
||||
|
||||
# Upnp action succeeds
|
||||
upnp_set_volume = upnp_get_action_mock(
|
||||
upnp_device, UPNP_SVC_RENDERING_CONTROL, "SetVolume"
|
||||
)
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5},
|
||||
True,
|
||||
)
|
||||
dmr_device.async_set_volume_level.assert_called_once_with(0.5)
|
||||
assert "Unable to set volume level on" not in caplog.text
|
||||
|
||||
# Upnp action failed
|
||||
upnp_set_volume.async_call.side_effect = UpnpActionResponseError(
|
||||
dmr_device.async_set_volume_level.reset_mock()
|
||||
dmr_device.async_set_volume_level.side_effect = UpnpActionResponseError(
|
||||
status=500, error_code=501, error_desc="Action Failed"
|
||||
)
|
||||
assert await hass.services.async_call(
|
||||
@ -1370,6 +1358,7 @@ async def test_volume_control_upnp(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
|
||||
True,
|
||||
)
|
||||
dmr_device.async_set_volume_level.assert_called_once_with(0.6)
|
||||
assert "Unable to set volume level on" in caplog.text
|
||||
|
||||
|
||||
@ -1390,7 +1379,7 @@ async def test_upnp_not_available(
|
||||
assert "Upnp services are not available" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "upnp_device", "rest_api")
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "upnp_factory")
|
||||
async def test_upnp_missing_service(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@ -1404,4 +1393,79 @@ async def test_upnp_missing_service(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
|
||||
True,
|
||||
)
|
||||
assert f"Upnp service {UPNP_SVC_RENDERING_CONTROL} is not available" in caplog.text
|
||||
assert "Upnp services are not available" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
async def test_upnp_shutdown(
|
||||
hass: HomeAssistant,
|
||||
dmr_device: Mock,
|
||||
upnp_notify_server: Mock,
|
||||
) -> None:
|
||||
"""Ensure that Upnp cleanup takes effect."""
|
||||
entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
assert await entry.async_unload(hass)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
dmr_device.async_unsubscribe_services.assert_called_once()
|
||||
upnp_notify_server.async_stop_server.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server")
|
||||
async def test_upnp_subscribe_events(hass: HomeAssistant, dmr_device: Mock) -> None:
|
||||
"""Test for Upnp event feedback."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.44
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is False
|
||||
|
||||
# DMR Devices gets updated, and raise event
|
||||
dmr_device.volume_level = 0
|
||||
dmr_device.is_volume_muted = True
|
||||
dmr_device.raise_event(None, None)
|
||||
|
||||
# State gets updated without the need to wait for next update
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
async def test_upnp_subscribe_events_upnperror(
|
||||
hass: HomeAssistant,
|
||||
dmr_device: Mock,
|
||||
upnp_notify_server: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test for failure to subscribe Upnp services."""
|
||||
with patch.object(dmr_device, "async_subscribe_services", side_effect=UpnpError):
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
|
||||
|
||||
upnp_notify_server.async_stop_server.assert_called_once()
|
||||
assert "Error while subscribing during device connect" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
async def test_upnp_subscribe_events_upnpresponseerror(
|
||||
hass: HomeAssistant,
|
||||
dmr_device: Mock,
|
||||
upnp_notify_server: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test for failure to subscribe Upnp services."""
|
||||
with patch.object(
|
||||
dmr_device,
|
||||
"async_subscribe_services",
|
||||
side_effect=UpnpResponseError(status=501),
|
||||
):
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
|
||||
|
||||
upnp_notify_server.async_stop_server.assert_not_called()
|
||||
assert "Device rejected subscription" in caplog.text
|
||||
|
Loading…
x
Reference in New Issue
Block a user