Home Assistant Unofficial Reference 2024.12.1
bridge.py
Go to the documentation of this file.
1 """samsungctl and samsungtvws bridge classes."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 import asyncio
7 from asyncio.exceptions import TimeoutError as AsyncioTimeoutError
8 from collections.abc import Callable, Iterable, Mapping
9 import contextlib
10 from datetime import datetime, timedelta
11 from typing import Any, cast
12 
13 from samsungctl import Remote
14 from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
15 from samsungtvws.async_remote import SamsungTVWSAsyncRemote
16 from samsungtvws.async_rest import SamsungTVAsyncRest
17 from samsungtvws.command import SamsungTVCommand
18 from samsungtvws.encrypted.command import SamsungTVEncryptedCommand
19 from samsungtvws.encrypted.remote import (
20  SamsungTVEncryptedWSAsyncRemote,
21  SendRemoteKey as SendEncryptedRemoteKey,
22 )
23 from samsungtvws.event import (
24  ED_INSTALLED_APP_EVENT,
25  MS_ERROR_EVENT,
26  parse_installed_app,
27 )
28 from samsungtvws.exceptions import (
29  ConnectionFailure,
30  HttpApiError,
31  ResponseError,
32  UnauthorizedError,
33 )
34 from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey
35 from websockets.exceptions import ConnectionClosedError, WebSocketException
36 
37 from homeassistant.const import (
38  CONF_DESCRIPTION,
39  CONF_HOST,
40  CONF_ID,
41  CONF_METHOD,
42  CONF_MODEL,
43  CONF_NAME,
44  CONF_PORT,
45  CONF_TIMEOUT,
46  CONF_TOKEN,
47 )
48 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
49 from homeassistant.helpers import entity_component
50 from homeassistant.helpers.aiohttp_client import async_get_clientsession
51 from homeassistant.helpers.device_registry import format_mac
52 from homeassistant.util import dt as dt_util
53 
54 from .const import (
55  CONF_SESSION_ID,
56  ENCRYPTED_WEBSOCKET_PORT,
57  LEGACY_PORT,
58  LOGGER,
59  METHOD_ENCRYPTED_WEBSOCKET,
60  METHOD_LEGACY,
61  METHOD_WEBSOCKET,
62  RESULT_AUTH_MISSING,
63  RESULT_CANNOT_CONNECT,
64  RESULT_NOT_SUPPORTED,
65  RESULT_SUCCESS,
66  SUCCESSFUL_RESULTS,
67  TIMEOUT_REQUEST,
68  TIMEOUT_WEBSOCKET,
69  VALUE_CONF_ID,
70  VALUE_CONF_NAME,
71  WEBSOCKET_PORTS,
72 )
73 
74 # Since the TV will take a few seconds to go to sleep
75 # and actually be seen as off, we need to wait just a bit
76 # more than the next scan interval
77 SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta(
78  seconds=5
79 )
80 
81 KEY_PRESS_TIMEOUT = 1.2
82 
83 ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400", "H6410"}
84 ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"}
85 
86 REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError)
87 
88 
89 def mac_from_device_info(info: dict[str, Any]) -> str | None:
90  """Extract the mac address from the device info."""
91  if wifi_mac := info.get("device", {}).get("wifiMac"):
92  return format_mac(wifi_mac)
93  return None
94 
95 
96 def model_requires_encryption(model: str | None) -> bool:
97  """H and J models need pairing with PIN."""
98  return model is not None and len(model) > 4 and model[4] in ("H", "J")
99 
100 
102  hass: HomeAssistant,
103  host: str,
104 ) -> tuple[str, int | None, str | None, dict[str, Any] | None]:
105  """Fetch the port, method, and device info."""
106  # Try the websocket ssl and non-ssl ports
107  for port in WEBSOCKET_PORTS:
108  bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port)
109  if info := await bridge.async_device_info():
110  LOGGER.debug(
111  "Fetching rest info via %s was successful: %s, checking for encrypted",
112  port,
113  info,
114  )
115  # Check the encrypted port if the model requires encryption
116  if model_requires_encryption(info.get("device", {}).get("modelName")):
117  encrypted_bridge = SamsungTVEncryptedBridge(
118  hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT
119  )
120  result = await encrypted_bridge.async_try_connect()
121  if result != RESULT_CANNOT_CONNECT:
122  return (
123  result,
124  ENCRYPTED_WEBSOCKET_PORT,
125  METHOD_ENCRYPTED_WEBSOCKET,
126  info,
127  )
128  return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info
129 
130  # Try legacy port
131  bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT)
132  result = await bridge.async_try_connect()
133  if result in SUCCESSFUL_RESULTS:
134  return result, LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info()
135 
136  # Failed to get info
137  return result, None, None, None
138 
139 
140 class SamsungTVBridge(ABC):
141  """The Base Bridge abstract class."""
142 
143  @staticmethod
145  hass: HomeAssistant,
146  method: str,
147  host: str,
148  port: int | None = None,
149  entry_data: Mapping[str, Any] | None = None,
150  ) -> SamsungTVBridge:
151  """Get Bridge instance."""
152  if method == METHOD_LEGACY or port == LEGACY_PORT:
153  return SamsungTVLegacyBridge(hass, method, host, port)
154  if method == METHOD_ENCRYPTED_WEBSOCKET or port == ENCRYPTED_WEBSOCKET_PORT:
155  return SamsungTVEncryptedBridge(hass, method, host, port, entry_data)
156  return SamsungTVWSBridge(hass, method, host, port, entry_data)
157 
158  def __init__(
159  self, hass: HomeAssistant, method: str, host: str, port: int | None = None
160  ) -> None:
161  """Initialize Bridge."""
162  self.hasshass = hass
163  self.portport = port
164  self.methodmethod = method
165  self.hosthost = host
166  self.token: str | None = None
167  self.session_id: str | None = None
168  self.auth_failed: bool = False
169  self._reauth_callback_reauth_callback: CALLBACK_TYPE | None = None
170  self._update_config_entry_update_config_entry: Callable[[Mapping[str, Any]], None] | None = None
171  self._app_list_callback_app_list_callback: Callable[[dict[str, str]], None] | None = None
172 
173  # Mark the end of a shutdown command (need to wait 15 seconds before
174  # sending the next command to avoid turning the TV back ON).
175  self._end_of_power_off_end_of_power_off: datetime | None = None
176 
177  def register_reauth_callback(self, func: CALLBACK_TYPE) -> None:
178  """Register a callback function."""
179  self._reauth_callback_reauth_callback = func
180 
182  self, func: Callable[[Mapping[str, Any]], None]
183  ) -> None:
184  """Register a callback function."""
185  self._update_config_entry_update_config_entry = func
186 
188  self, func: Callable[[dict[str, str]], None]
189  ) -> None:
190  """Register app_list callback function."""
191  self._app_list_callback_app_list_callback = func
192 
193  @abstractmethod
194  async def async_try_connect(self) -> str:
195  """Try to connect to the TV."""
196 
197  @abstractmethod
198  async def async_device_info(self) -> dict[str, Any] | None:
199  """Try to gather infos of this TV."""
200 
201  async def async_request_app_list(self) -> None:
202  """Request app list."""
203  # Overridden in SamsungTVWSBridge
204  LOGGER.debug(
205  "App list request is not supported on %s TV: %s",
206  self.method,
207  self.host,
208  )
209  self._notify_app_list_callback({})
210 
211  @abstractmethod
212  async def async_is_on(self) -> bool:
213  """Tells if the TV is on."""
214 
215  @abstractmethod
216  async def async_send_keys(self, keys: list[str]) -> None:
217  """Send a list of keys to the tv."""
218 
219  @property
220  def power_off_in_progress(self) -> bool:
221  """Return if power off has been recently requested."""
222  return (
223  self._end_of_power_off_end_of_power_off is not None
224  and self._end_of_power_off_end_of_power_off > dt_util.utcnow()
225  )
226 
227  async def async_power_off(self) -> None:
228  """Send power off command to remote and close."""
229  self._end_of_power_off_end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME
230  await self._async_send_power_off_async_send_power_off()
231  # Force closing of remote session to provide instant UI feedback
232  await self.async_close_remoteasync_close_remote()
233 
234  @abstractmethod
235  async def _async_send_power_off(self) -> None:
236  """Send power off command."""
237 
238  @abstractmethod
239  async def async_close_remote(self) -> None:
240  """Close remote object."""
241 
242  def _notify_reauth_callback(self) -> None:
243  """Notify access denied callback."""
244  if self._reauth_callback is not None:
245  self._reauth_callback()
246 
247  def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None:
248  """Notify update config callback."""
249  if self._update_config_entry_update_config_entry is not None:
250  self._update_config_entry_update_config_entry(updates)
251 
252  def _notify_app_list_callback(self, app_list: dict[str, str]) -> None:
253  """Notify update config callback."""
254  if self._app_list_callback_app_list_callback is not None:
255  self._app_list_callback_app_list_callback(app_list)
256 
257 
259  """The Bridge for Legacy TVs."""
260 
261  def __init__(
262  self, hass: HomeAssistant, method: str, host: str, port: int | None
263  ) -> None:
264  """Initialize Bridge."""
265  super().__init__(hass, method, host, LEGACY_PORT)
266  self.configconfig = {
267  CONF_NAME: VALUE_CONF_NAME,
268  CONF_DESCRIPTION: VALUE_CONF_NAME,
269  CONF_ID: VALUE_CONF_ID,
270  CONF_HOST: host,
271  CONF_METHOD: method,
272  CONF_PORT: None,
273  CONF_TIMEOUT: 1,
274  }
275  self._remote_remote: Remote | None = None
276 
277  async def async_is_on(self) -> bool:
278  """Tells if the TV is on."""
279  return await self.hasshass.async_add_executor_job(self._is_on_is_on)
280 
281  def _is_on(self) -> bool:
282  """Tells if the TV is on."""
283  if self._remote_remote is not None:
284  self._close_remote_close_remote()
285 
286  try:
287  return self._get_remote_get_remote() is not None
288  except (UnhandledResponse, AccessDenied):
289  # We got a response so it's working.
290  return True
291 
292  async def async_try_connect(self) -> str:
293  """Try to connect to the Legacy TV."""
294  return await self.hasshass.async_add_executor_job(self._try_connect_try_connect)
295 
296  def _try_connect(self) -> str:
297  """Try to connect to the Legacy TV."""
298  config = {
299  CONF_NAME: VALUE_CONF_NAME,
300  CONF_DESCRIPTION: VALUE_CONF_NAME,
301  CONF_ID: VALUE_CONF_ID,
302  CONF_HOST: self.hosthost,
303  CONF_METHOD: self.methodmethod,
304  CONF_PORT: None,
305  # We need this high timeout because waiting for auth popup
306  # is just an open socket
307  CONF_TIMEOUT: TIMEOUT_REQUEST,
308  }
309  try:
310  LOGGER.debug("Try config: %s", config)
311  with Remote(config.copy()):
312  LOGGER.debug("Working config: %s", config)
313  return RESULT_SUCCESS
314  except AccessDenied:
315  LOGGER.debug("Working but denied config: %s", config)
316  return RESULT_AUTH_MISSING
317  except UnhandledResponse as err:
318  LOGGER.debug("Working but unsupported config: %s, error: %s", config, err)
319  return RESULT_NOT_SUPPORTED
320  except (ConnectionClosed, OSError) as err:
321  LOGGER.debug("Failing config: %s, error: %s", config, err)
322  return RESULT_CANNOT_CONNECT
323 
324  async def async_device_info(self) -> dict[str, Any] | None:
325  """Try to gather infos of this device."""
326  return None
327 
328  def _notify_reauth_callback(self) -> None:
329  """Notify access denied callback."""
330  if self._reauth_callback_reauth_callback is not None:
331  self.hasshass.loop.call_soon_threadsafe(self._reauth_callback_reauth_callback)
332 
333  def _get_remote(self) -> Remote:
334  """Create or return a remote control instance."""
335  if self._remote_remote is None:
336  # We need to create a new instance to reconnect.
337  try:
338  LOGGER.debug("Create SamsungTVLegacyBridge for %s", self.hosthost)
339  self._remote_remote = Remote(self.configconfig.copy())
340  # This is only happening when the auth was switched to DENY
341  # A removed auth will lead to socket timeout because waiting
342  # for auth popup is just an open socket
343  except AccessDenied:
344  self.auth_failedauth_failed = True
345  self._notify_reauth_callback_notify_reauth_callback_notify_reauth_callback()
346  raise
347  except (ConnectionClosed, OSError):
348  pass
349  return self._remote_remote
350 
351  async def async_send_keys(self, keys: list[str]) -> None:
352  """Send a list of keys using legacy protocol."""
353  first_key = True
354  for key in keys:
355  if first_key:
356  first_key = False
357  else:
358  await asyncio.sleep(KEY_PRESS_TIMEOUT)
359  await self.hasshass.async_add_executor_job(self._send_key_send_key, key)
360 
361  def _send_key(self, key: str) -> None:
362  """Send a key using legacy protocol."""
363  try:
364  # recreate connection if connection was dead
365  retry_count = 1
366  for _ in range(retry_count + 1):
367  try:
368  if remote := self._get_remote_get_remote():
369  remote.control(key)
370  break
371  except (ConnectionClosed, BrokenPipeError):
372  # BrokenPipe can occur when the commands is sent to fast
373  self._remote_remote = None
374  except (UnhandledResponse, AccessDenied):
375  # We got a response so it's on.
376  LOGGER.debug("Failed sending command %s", key, exc_info=True)
377  except OSError:
378  # Different reasons, e.g. hostname not resolveable
379  pass
380 
381  async def _async_send_power_off(self) -> None:
382  """Send power off command to remote."""
383  await self.async_send_keysasync_send_keysasync_send_keys(["KEY_POWEROFF"])
384 
385  async def async_close_remote(self) -> None:
386  """Close remote object."""
387  await self.hasshass.async_add_executor_job(self._close_remote_close_remote)
388 
389  def _close_remote(self) -> None:
390  """Close remote object."""
391  try:
392  if self._remote_remote is not None:
393  # Close the current remote connection
394  self._remote_remote.close()
395  self._remote_remote = None
396  except OSError:
397  LOGGER.debug("Could not establish connection")
398 
399 
401  _RemoteT: (SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote),
402  _CommandT: (SamsungTVCommand, SamsungTVEncryptedCommand),
403 ](SamsungTVBridge):
404  """The Bridge for WebSocket TVs (v1/v2)."""
405 
406  def __init__(
407  self,
408  hass: HomeAssistant,
409  method: str,
410  host: str,
411  port: int | None = None,
412  ) -> None:
413  """Initialize Bridge."""
414  super().__init__(hass, method, host, port)
415  self._remote: _RemoteT | None = None
416  self._remote_lock = asyncio.Lock()
417 
418  async def async_is_on(self) -> bool:
419  """Tells if the TV is on."""
420  LOGGER.debug("Checking if TV %s is on using websocket", self.host)
421  if remote := await self._async_get_remote():
422  return remote.is_alive()
423  return False
424 
425  async def _async_send_commands(self, commands: list[_CommandT]) -> None:
426  """Send the commands using websocket protocol."""
427  try:
428  # recreate connection if connection was dead
429  retry_count = 1
430  for _ in range(retry_count + 1):
431  try:
432  if remote := await self._async_get_remote():
433  await remote.send_commands(commands) # type: ignore[arg-type]
434  break
435  except (
436  BrokenPipeError,
437  WebSocketException,
438  ):
439  # BrokenPipe can occur when the commands is sent to fast
440  # WebSocketException can occur when timed out
441  self._remote = None
442  except OSError:
443  # Different reasons, e.g. hostname not resolveable
444  pass
445 
446  async def _async_get_remote(self) -> _RemoteT | None:
447  """Create or return a remote control instance."""
448  if (remote := self._remote) and remote.is_alive():
449  # If we have one then try to use it
450  return remote
451 
452  async with self._remote_lock:
453  # If we don't have one make sure we do it under the lock
454  # so we don't make two do due a race to get the remote
455  return await self._async_get_remote_under_lock()
456 
457  @abstractmethod
458  async def _async_get_remote_under_lock(self) -> _RemoteT | None:
459  """Create or return a remote control instance."""
460 
461  async def async_close_remote(self) -> None:
462  """Close remote object."""
463  try:
464  if self._remote is not None:
465  # Close the current remote connection
466  await self._remote.close()
467  self._remote = None
468  except OSError as err:
469  LOGGER.debug("Error closing connection to %s: %s", self.host, err)
470 
471 
473  SamsungTVWSBaseBridge[SamsungTVWSAsyncRemote, SamsungTVCommand]
474 ):
475  """The Bridge for WebSocket TVs (v2)."""
476 
477  def __init__(
478  self,
479  hass: HomeAssistant,
480  method: str,
481  host: str,
482  port: int | None = None,
483  entry_data: Mapping[str, Any] | None = None,
484  ) -> None:
485  """Initialize Bridge."""
486  super().__init__(hass, method, host, port)
487  if entry_data:
488  self.tokentoken = entry_data.get(CONF_TOKEN)
489  self._rest_api: SamsungTVAsyncRest | None = None
490  self._device_info_device_info: dict[str, Any] | None = None
491 
492  def _get_device_spec(self, key: str) -> Any | None:
493  """Check if a flag exists in latest device info."""
494  if not ((info := self._device_info_device_info) and (device := info.get("device"))):
495  return None
496  return device.get(key)
497 
498  async def async_is_on(self) -> bool:
499  """Tells if the TV is on."""
500  # On some TVs, opening a websocket turns on the TV
501  # so first check "PowerState" if device_info has it
502  # then fallback to default, trying to open a websocket
503  if self._get_device_spec_get_device_spec("PowerState") is not None:
504  LOGGER.debug("Checking if TV %s is on using device info", self.host)
505  # Ensure we get an updated value
506  info = await self.async_device_infoasync_device_info(force=True)
507  return info is not None and info["device"]["PowerState"] == "on"
508 
509  return await super().async_is_on()
510 
511  async def async_try_connect(self) -> str:
512  """Try to connect to the Websocket TV."""
513  for self.port in WEBSOCKET_PORTS:
514  config = {
515  CONF_NAME: VALUE_CONF_NAME,
516  CONF_HOST: self.host,
517  CONF_METHOD: self.method,
518  CONF_PORT: self.port,
519  # We need this high timeout because waiting for auth popup
520  # is just an open socket
521  CONF_TIMEOUT: TIMEOUT_REQUEST,
522  }
523 
524  result = None
525  try:
526  LOGGER.debug("Try config: %s", config)
527  async with SamsungTVWSAsyncRemote(
528  host=self.host,
529  port=self.port,
530  token=self.tokentoken,
531  timeout=TIMEOUT_REQUEST,
532  name=VALUE_CONF_NAME,
533  ) as remote:
534  await remote.open()
535  self.tokentoken = remote.token
536  LOGGER.debug("Working config: %s", config)
537  return RESULT_SUCCESS
538  except ConnectionClosedError as err:
539  LOGGER.warning(
540  (
541  "Working but unsupported config: %s, error: '%s'; this may be"
542  " an indication that access to the TV has been denied. Please"
543  " check the Device Connection Manager on your TV"
544  ),
545  config,
546  err,
547  )
548  result = RESULT_NOT_SUPPORTED
549  except WebSocketException as err:
550  LOGGER.debug(
551  "Working but unsupported config: %s, error: %s", config, err
552  )
553  result = RESULT_NOT_SUPPORTED
554  except UnauthorizedError as err:
555  LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err)
556  return RESULT_AUTH_MISSING
557  except (ConnectionFailure, OSError, AsyncioTimeoutError) as err:
558  LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err)
559  else: # noqa: PLW0120
560  if result:
561  return result
562 
563  return RESULT_CANNOT_CONNECT
564 
565  async def async_device_info(self, force: bool = False) -> dict[str, Any] | None:
566  """Try to gather infos of this TV."""
567  if self._rest_api is None:
568  assert self.port
569  rest_api = SamsungTVAsyncRest(
570  host=self.host,
571  session=async_get_clientsession(self.hass),
572  port=self.port,
573  timeout=TIMEOUT_WEBSOCKET,
574  )
575 
576  with contextlib.suppress(*REST_EXCEPTIONS):
577  device_info: dict[str, Any] = await rest_api.rest_device_info()
578  LOGGER.debug("Device info on %s is: %s", self.host, device_info)
579  self._device_info_device_info = device_info
580  return device_info
581 
582  return None if force else self._device_info_device_info
583 
584  async def async_launch_app(self, app_id: str) -> None:
585  """Send the launch_app command using websocket protocol."""
586  await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)])
587 
588  async def async_request_app_list(self) -> None:
589  """Get installed app list."""
590  await self._async_send_commands([ChannelEmitCommand.get_installed_app()])
591 
592  async def async_send_keys(self, keys: list[str]) -> None:
593  """Send a list of keys using websocket protocol."""
594  await self._async_send_commands([SendRemoteKey.click(key) for key in keys])
595 
596  async def _async_get_remote_under_lock(self) -> SamsungTVWSAsyncRemote | None:
597  """Create or return a remote control instance."""
598  if self._remote_remote is None or not self._remote_remote.is_alive():
599  # We need to create a new instance to reconnect.
600  LOGGER.debug("Create SamsungTVWSBridge for %s", self.host)
601  assert self.port
602  self._remote_remote = SamsungTVWSAsyncRemote(
603  host=self.host,
604  port=self.port,
605  token=self.tokentoken,
606  timeout=TIMEOUT_WEBSOCKET,
607  name=VALUE_CONF_NAME,
608  )
609  try:
610  await self._remote_remote.start_listening(self._remote_event_remote_event)
611  except UnauthorizedError as err:
612  LOGGER.warning(
613  "Failed to get remote for %s, re-authentication required: %s",
614  self.host,
615  repr(err),
616  )
617  self.auth_failedauth_failed = True
618  self._notify_reauth_callback()
619  self._remote_remote = None
620  except ConnectionClosedError as err:
621  LOGGER.warning(
622  "Failed to get remote for %s: %s",
623  self.host,
624  repr(err),
625  )
626  self._remote_remote = None
627  except ConnectionFailure as err:
628  LOGGER.warning(
629  (
630  "Unexpected ConnectionFailure trying to get remote for %s, "
631  "please report this issue: %s"
632  ),
633  self.host,
634  repr(err),
635  )
636  self._remote_remote = None
637  except (WebSocketException, AsyncioTimeoutError, OSError) as err:
638  LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err))
639  self._remote_remote = None
640  else:
641  LOGGER.debug("Created SamsungTVWSBridge for %s", self.host)
642  if self._device_info_device_info is None:
643  # Initialise device info on first connect
644  await self.async_device_infoasync_device_info()
645  if self.tokentoken != self._remote_remote.token:
646  LOGGER.warning(
647  "SamsungTVWSBridge has provided a new token %s",
648  self._remote_remote.token,
649  )
650  self.tokentoken = self._remote_remote.token
651  self._notify_update_config_entry({CONF_TOKEN: self.tokentoken})
652  return self._remote_remote
653 
654  def _remote_event(self, event: str, response: Any) -> None:
655  """Received event from remote websocket."""
656  if event == ED_INSTALLED_APP_EVENT:
657  self._notify_app_list_callback(
658  {
659  app["name"]: app["appId"]
660  for app in sorted(
661  parse_installed_app(response),
662  key=lambda app: cast(str, app["name"]),
663  )
664  }
665  )
666  return
667  if event == MS_ERROR_EVENT:
668  # { 'event': 'ms.error',
669  # 'data': {'message': 'unrecognized method value : ms.remote.control'}}
670  if (data := response.get("data")) and (
671  message := data.get("message")
672  ) == "unrecognized method value : ms.remote.control":
673  LOGGER.error(
674  (
675  "Your TV seems to be unsupported by SamsungTVWSBridge"
676  " and needs a PIN: '%s'. Updating config entry"
677  ),
678  message,
679  )
680  self._notify_update_config_entry(
681  {
682  CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET,
683  CONF_PORT: ENCRYPTED_WEBSOCKET_PORT,
684  }
685  )
686 
687  async def _async_send_power_off(self) -> None:
688  """Send power off command to remote."""
689  if self._get_device_spec_get_device_spec("FrameTVSupport") == "true":
690  await self._async_send_commands(SendRemoteKey.hold("KEY_POWER", 3))
691  else:
692  await self._async_send_commands([SendRemoteKey.click("KEY_POWER")])
693 
694 
696  SamsungTVWSBaseBridge[SamsungTVEncryptedWSAsyncRemote, SamsungTVEncryptedCommand]
697 ):
698  """The Bridge for Encrypted WebSocket TVs (v1 - J/H models)."""
699 
700  def __init__(
701  self,
702  hass: HomeAssistant,
703  method: str,
704  host: str,
705  port: int | None = None,
706  entry_data: Mapping[str, Any] | None = None,
707  ) -> None:
708  """Initialize Bridge."""
709  super().__init__(hass, method, host, port)
710  self._power_off_warning_logged_power_off_warning_logged: bool = False
711  self._model_model: str | None = None
712  self._short_model_short_model: str | None = None
713  if entry_data:
714  self.tokentoken = entry_data.get(CONF_TOKEN)
715  self.session_idsession_id = entry_data.get(CONF_SESSION_ID)
716  self._model_model = entry_data.get(CONF_MODEL)
717  if self._model_model and len(self._model_model) > 4:
718  self._short_model_short_model = self._model_model[4:]
719 
720  self._rest_api_port_rest_api_port: int | None = None
721  self._device_info_device_info: dict[str, Any] | None = None
722 
723  async def async_try_connect(self) -> str:
724  """Try to connect to the Websocket TV."""
725  self.portport = ENCRYPTED_WEBSOCKET_PORT
726  config = {
727  CONF_NAME: VALUE_CONF_NAME,
728  CONF_HOST: self.host,
729  CONF_METHOD: self.method,
730  CONF_PORT: self.portport,
731  CONF_TIMEOUT: TIMEOUT_WEBSOCKET,
732  }
733 
734  try:
735  LOGGER.debug("Try config: %s", config)
736  async with SamsungTVEncryptedWSAsyncRemote(
737  host=self.host,
738  port=self.portport,
739  web_session=async_get_clientsession(self.hass),
740  token=self.tokentoken or "",
741  session_id=self.session_idsession_id or "",
742  timeout=TIMEOUT_REQUEST,
743  ) as remote:
744  await remote.start_listening()
745  except WebSocketException as err:
746  LOGGER.debug("Working but unsupported config: %s, error: %s", config, err)
747  return RESULT_NOT_SUPPORTED
748  except (OSError, AsyncioTimeoutError, ConnectionFailure) as err:
749  LOGGER.debug("Failing config: %s, error: %s", config, err)
750  else:
751  LOGGER.debug("Working config: %s", config)
752  return RESULT_SUCCESS
753 
754  return RESULT_CANNOT_CONNECT
755 
756  async def async_device_info(self) -> dict[str, Any] | None:
757  """Try to gather infos of this TV."""
758  # Default to try all ports
759  rest_api_ports: Iterable[int] = WEBSOCKET_PORTS
760  if self._rest_api_port_rest_api_port:
761  # We have already made a successful call to the REST api
762  rest_api_ports = (self._rest_api_port_rest_api_port,)
763 
764  for rest_api_port in rest_api_ports:
765  assert self.portport
766  rest_api = SamsungTVAsyncRest(
767  host=self.host,
768  session=async_get_clientsession(self.hass),
769  port=rest_api_port,
770  timeout=TIMEOUT_WEBSOCKET,
771  )
772 
773  with contextlib.suppress(*REST_EXCEPTIONS):
774  device_info: dict[str, Any] = await rest_api.rest_device_info()
775  LOGGER.debug("Device info on %s is: %s", self.host, device_info)
776  self._device_info_device_info = device_info
777  self._rest_api_port_rest_api_port = rest_api_port
778  return device_info
779 
780  return self._device_info_device_info
781 
782  async def async_send_keys(self, keys: list[str]) -> None:
783  """Send a list of keys using websocket protocol."""
784  await self._async_send_commands(
785  [SendEncryptedRemoteKey.click(key) for key in keys]
786  )
787 
789  self,
790  ) -> SamsungTVEncryptedWSAsyncRemote | None:
791  """Create or return a remote control instance."""
792  if self._remote_remote is None or not self._remote_remote.is_alive():
793  # We need to create a new instance to reconnect.
794  LOGGER.debug("Create SamsungTVEncryptedBridge for %s", self.host)
795  assert self.portport
796  self._remote_remote = SamsungTVEncryptedWSAsyncRemote(
797  host=self.host,
798  port=self.portport,
799  web_session=async_get_clientsession(self.hass),
800  token=self.tokentoken or "",
801  session_id=self.session_idsession_id or "",
802  timeout=TIMEOUT_WEBSOCKET,
803  )
804  try:
805  await self._remote_remote.start_listening()
806  except (WebSocketException, AsyncioTimeoutError, OSError) as err:
807  LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err))
808  self._remote_remote = None
809  else:
810  LOGGER.debug("Created SamsungTVEncryptedBridge for %s", self.host)
811  return self._remote_remote
812 
813  async def _async_send_power_off(self) -> None:
814  """Send power off command to remote."""
815  power_off_commands: list[SamsungTVEncryptedCommand] = []
816  if self._short_model_short_model in ENCRYPTED_MODEL_USES_POWER_OFF:
817  power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF"))
818  elif self._short_model_short_model in ENCRYPTED_MODEL_USES_POWER:
819  power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER"))
820  else:
821  if self._model_model and not self._power_off_warning_logged_power_off_warning_logged:
822  LOGGER.warning(
823  (
824  "Unknown power_off command for %s (%s): sending KEY_POWEROFF"
825  " and KEY_POWER"
826  ),
827  self._model_model,
828  self.host,
829  )
830  self._power_off_warning_logged_power_off_warning_logged = True
831  power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF"))
832  power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER"))
833  await self._async_send_commands(power_off_commands)
None _notify_update_config_entry(self, Mapping[str, Any] updates)
Definition: bridge.py:247
None register_update_config_entry_callback(self, Callable[[Mapping[str, Any]], None] func)
Definition: bridge.py:183
SamsungTVBridge get_bridge(HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
Definition: bridge.py:150
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None)
Definition: bridge.py:160
None register_reauth_callback(self, CALLBACK_TYPE func)
Definition: bridge.py:177
None register_app_list_callback(self, Callable[[dict[str, str]], None] func)
Definition: bridge.py:189
None _notify_app_list_callback(self, dict[str, str] app_list)
Definition: bridge.py:252
SamsungTVEncryptedWSAsyncRemote|None _async_get_remote_under_lock(self)
Definition: bridge.py:790
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
Definition: bridge.py:707
None __init__(self, HomeAssistant hass, str method, str host, int|None port)
Definition: bridge.py:263
SamsungTVWSAsyncRemote|None _async_get_remote_under_lock(self)
Definition: bridge.py:596
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
Definition: bridge.py:484
None _remote_event(self, str event, Any response)
Definition: bridge.py:654
dict[str, Any]|None async_device_info(self, bool force=False)
Definition: bridge.py:565
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
tuple[str, int|None, str|None, dict[str, Any]|None] async_get_device_info(HomeAssistant hass, str host)
Definition: bridge.py:104
None _async_send_commands(self, list[_CommandT] commands)
Definition: bridge.py:425
_RemoteT|None _async_get_remote(self)
Definition: bridge.py:446
_RemoteT|None _async_get_remote_under_lock(self)
Definition: bridge.py:458
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None)
Definition: bridge.py:412
str|None mac_from_device_info(dict[str, Any] info)
Definition: bridge.py:89
bool model_requires_encryption(str|None model)
Definition: bridge.py:96
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)