1 """Support for interface with an Samsung TV."""
3 from __future__
import annotations
6 from collections.abc
import Sequence
9 from async_upnp_client.aiohttp
import AiohttpNotifyServer, AiohttpSessionRequester
10 from async_upnp_client.client
import UpnpDevice, UpnpService, UpnpStateVariable
11 from async_upnp_client.client_factory
import UpnpFactory
12 from async_upnp_client.exceptions
import (
13 UpnpActionResponseError,
14 UpnpCommunicationError,
20 from async_upnp_client.profiles.dlna
import DmrDevice
21 from async_upnp_client.utils
import async_get_local_ip
22 import voluptuous
as vol
25 MediaPlayerDeviceClass,
27 MediaPlayerEntityFeature,
37 from .
import SamsungTVConfigEntry
38 from .bridge
import SamsungTVWSBridge
39 from .const
import CONF_SSDP_RENDERING_CONTROL_LOCATION, LOGGER
40 from .coordinator
import SamsungTVDataUpdateCoordinator
41 from .entity
import SamsungTVEntity
43 SOURCES = {
"TV":
"KEY_TV",
"HDMI":
"KEY_HDMI"}
46 MediaPlayerEntityFeature.NEXT_TRACK
47 | MediaPlayerEntityFeature.PAUSE
48 | MediaPlayerEntityFeature.PLAY
49 | MediaPlayerEntityFeature.PLAY_MEDIA
50 | MediaPlayerEntityFeature.PREVIOUS_TRACK
51 | MediaPlayerEntityFeature.SELECT_SOURCE
52 | MediaPlayerEntityFeature.STOP
53 | MediaPlayerEntityFeature.TURN_OFF
54 | MediaPlayerEntityFeature.VOLUME_MUTE
55 | MediaPlayerEntityFeature.VOLUME_SET
56 | MediaPlayerEntityFeature.VOLUME_STEP
66 entry: SamsungTVConfigEntry,
67 async_add_entities: AddEntitiesCallback,
69 """Set up the Samsung TV from a config entry."""
70 coordinator = entry.runtime_data
75 """Representation of a Samsung TV."""
77 _attr_source_list: list[str]
79 _attr_device_class = MediaPlayerDeviceClass.TV
81 def __init__(self, coordinator: SamsungTVDataUpdateCoordinator) ->
None:
82 """Initialize the Samsung device."""
83 super().
__init__(coordinator=coordinator)
84 self._ssdp_rendering_control_location: str |
None = (
85 coordinator.config_entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION)
92 self.
_app_list_app_list: dict[str, str] |
None =
None
93 self._app_list_event: asyncio.Event = asyncio.Event()
100 if self._ssdp_rendering_control_location:
105 self.
_dmr_device_dmr_device: DmrDevice |
None =
None
106 self.
_upnp_server_upnp_server: AiohttpNotifyServer |
None =
None
110 """Flag media player features that are supported."""
123 """App list callback."""
126 self._app_list_event.set()
129 """Run when entity about to be added to hass."""
133 if self.coordinator.is_on:
140 """Handle removal."""
141 self.coordinator.async_extra_update =
None
146 """Handle data update."""
147 if self.coordinator.is_on:
155 """Update state of device."""
156 if not self.coordinator.is_on:
158 await self.
_dmr_device_dmr_device.async_unsubscribe_services()
161 startup_tasks: list[asyncio.Task[Any]] = []
163 if not self._app_list_event.is_set():
168 if not self.
_dmr_device_dmr_device
and self._ssdp_rendering_control_location:
172 await asyncio.gather(*startup_tasks)
179 if (dmr_device := self.
_dmr_device_dmr_device)
is None:
185 volume_level := dmr_device.volume_level
191 is_muted := dmr_device.is_volume_muted
199 await self.
_bridge_bridge.async_request_app_list()
200 if self._app_list_event.is_set():
205 async
with asyncio.timeout(APP_LIST_DELAY):
206 await self._app_list_event.wait()
207 except TimeoutError
as err:
209 self._app_list_event.set()
210 LOGGER.debug(
"Failed to load app list from %s: %r", self._host, err)
213 assert self._ssdp_rendering_control_location
is not None
216 upnp_requester = AiohttpSessionRequester(session)
220 upnp_factory = UpnpFactory(upnp_requester, non_strict=
True)
221 upnp_device: UpnpDevice |
None =
None
223 upnp_device = await upnp_factory.async_create_device(
224 self._ssdp_rendering_control_location
226 except (UpnpConnectionError, UpnpResponseError, UpnpXmlContentError)
as err:
227 LOGGER.debug(
"Unable to create Upnp DMR device: %r", err, exc_info=
True)
229 _, event_ip = await async_get_local_ip(
230 self._ssdp_rendering_control_location, self.
hasshasshass.loop
232 source = (event_ip
or "0.0.0.0", 0)
234 requester=upnp_requester,
239 await self.
_upnp_server_upnp_server.async_start_server()
244 await self.
_dmr_device_dmr_device.async_subscribe_services(auto_resubscribe=
True)
245 except UpnpResponseError
as err:
248 LOGGER.debug(
"Device rejected subscription: %r", err)
249 except UpnpError
as err:
255 LOGGER.debug(
"Error while subscribing during device connect: %r", err)
261 await self.
_dmr_device_dmr_device.async_subscribe_services(auto_resubscribe=
True)
262 except UpnpCommunicationError
as err:
263 LOGGER.debug(
"Device rejected re-subscription: %r", err, exc_info=
True)
266 """Handle removal."""
267 if (dmr_device := self.
_dmr_device_dmr_device)
is not None:
269 dmr_device.on_event =
None
270 await dmr_device.async_unsubscribe_services()
272 if (upnp_server := self.
_upnp_server_upnp_server)
is not None:
274 await upnp_server.async_stop_server()
277 self, service: UpnpService, state_variables: Sequence[UpnpStateVariable]
279 """State variable(s) changed, let home-assistant know."""
285 """Send launch_app to the tv."""
286 if self.
_bridge_bridge.power_off_in_progress:
287 LOGGER.debug(
"TV is powering off, not sending launch_app command")
289 assert isinstance(self.
_bridge_bridge, SamsungTVWSBridge)
290 await self.
_bridge_bridge.async_launch_app(app_id)
293 """Send a key to the tv and handles exceptions."""
295 if self.
_bridge_bridge.power_off_in_progress
and keys[0] !=
"KEY_POWEROFF":
296 LOGGER.debug(
"TV is powering off, not sending keys: %s", keys)
298 await self.
_bridge_bridge.async_send_keys(keys)
301 """Turn off media player."""
305 """Set volume level on the media player."""
306 if (dmr_device := self.
_dmr_device_dmr_device)
is None:
307 LOGGER.warning(
"Upnp services are not available on %s", self._host)
310 await dmr_device.async_set_volume_level(volume)
311 except UpnpActionResponseError
as err:
312 LOGGER.warning(
"Unable to set volume level on %s: %r", self._host, err)
315 """Volume up the media player."""
319 """Volume down media player."""
323 """Send mute command."""
327 """Simulate play pause media player."""
334 """Send play command."""
339 """Send media pause command to media player."""
344 """Send next track command."""
348 """Send the previous track command."""
352 self, media_type: MediaType | str, media_id: str, **kwargs: Any
354 """Support changing a channel."""
355 if media_type == MediaType.APP:
359 if media_type != MediaType.CHANNEL:
360 LOGGER.error(
"Unsupported media type")
365 cv.positive_int(media_id)
367 LOGGER.error(
"Media ID must be positive integer")
371 keys=[f
"KEY_{digit}" for digit
in media_id] + [
"KEY_ENTER"]
375 """Turn the media player on."""
379 """Select input source."""
384 if source
in SOURCES:
388 LOGGER.error(
"Unsupported source")
None _async_turn_off(self)
None _async_turn_on(self)
None async_write_ha_state(self)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)