1 """samsungctl and samsungtvws bridge classes."""
3 from __future__
import annotations
5 from abc
import ABC, abstractmethod
7 from asyncio.exceptions
import TimeoutError
as AsyncioTimeoutError
8 from collections.abc
import Callable, Iterable, Mapping
10 from datetime
import datetime, timedelta
11 from typing
import Any, cast
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,
23 from samsungtvws.event
import (
24 ED_INSTALLED_APP_EVENT,
28 from samsungtvws.exceptions
import (
34 from samsungtvws.remote
import ChannelEmitCommand, SendRemoteKey
35 from websockets.exceptions
import ConnectionClosedError, WebSocketException
56 ENCRYPTED_WEBSOCKET_PORT,
59 METHOD_ENCRYPTED_WEBSOCKET,
63 RESULT_CANNOT_CONNECT,
77 SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL +
timedelta(
81 KEY_PRESS_TIMEOUT = 1.2
83 ENCRYPTED_MODEL_USES_POWER_OFF = {
"H6400",
"H6410"}
84 ENCRYPTED_MODEL_USES_POWER = {
"JU6400",
"JU641D"}
86 REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError)
90 """Extract the mac address from the device info."""
91 if wifi_mac := info.get(
"device", {}).
get(
"wifiMac"):
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")
104 ) -> tuple[str, int |
None, str |
None, dict[str, Any] |
None]:
105 """Fetch the port, method, and device info."""
107 for port
in WEBSOCKET_PORTS:
108 bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port)
109 if info := await bridge.async_device_info():
111 "Fetching rest info via %s was successful: %s, checking for encrypted",
118 hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT
120 result = await encrypted_bridge.async_try_connect()
121 if result != RESULT_CANNOT_CONNECT:
124 ENCRYPTED_WEBSOCKET_PORT,
125 METHOD_ENCRYPTED_WEBSOCKET,
128 return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info
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()
137 return result,
None,
None,
None
141 """The Base Bridge abstract class."""
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:
154 if method == METHOD_ENCRYPTED_WEBSOCKET
or port == ENCRYPTED_WEBSOCKET_PORT:
159 self, hass: HomeAssistant, method: str, host: str, port: int |
None =
None
161 """Initialize Bridge."""
166 self.token: str |
None =
None
167 self.session_id: str |
None =
None
168 self.auth_failed: bool =
False
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
178 """Register a callback function."""
182 self, func: Callable[[Mapping[str, Any]],
None]
184 """Register a callback function."""
188 self, func: Callable[[dict[str, str]],
None]
190 """Register app_list callback function."""
195 """Try to connect to the TV."""
199 """Try to gather infos of this TV."""
202 """Request app list."""
205 "App list request is not supported on %s TV: %s",
209 self._notify_app_list_callback({})
213 """Tells if the TV is on."""
217 """Send a list of keys to the tv."""
221 """Return if power off has been recently requested."""
228 """Send power off command to remote and close."""
236 """Send power off command."""
240 """Close remote object."""
243 """Notify access denied callback."""
244 if self._reauth_callback
is not None:
245 self._reauth_callback()
248 """Notify update config callback."""
253 """Notify update config callback."""
259 """The Bridge for Legacy TVs."""
262 self, hass: HomeAssistant, method: str, host: str, port: int |
None
264 """Initialize Bridge."""
265 super().
__init__(hass, method, host, LEGACY_PORT)
267 CONF_NAME: VALUE_CONF_NAME,
268 CONF_DESCRIPTION: VALUE_CONF_NAME,
269 CONF_ID: VALUE_CONF_ID,
275 self.
_remote_remote: Remote |
None =
None
278 """Tells if the TV is on."""
279 return await self.
hasshass.async_add_executor_job(self.
_is_on_is_on)
282 """Tells if the TV is on."""
283 if self.
_remote_remote
is not None:
288 except (UnhandledResponse, AccessDenied):
293 """Try to connect to the Legacy TV."""
294 return await self.
hasshass.async_add_executor_job(self.
_try_connect_try_connect)
297 """Try to connect to the Legacy TV."""
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,
307 CONF_TIMEOUT: TIMEOUT_REQUEST,
310 LOGGER.debug(
"Try config: %s", config)
311 with Remote(config.copy()):
312 LOGGER.debug(
"Working config: %s", config)
313 return RESULT_SUCCESS
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
325 """Try to gather infos of this device."""
329 """Notify access denied callback."""
334 """Create or return a remote control instance."""
335 if self.
_remote_remote
is None:
338 LOGGER.debug(
"Create SamsungTVLegacyBridge for %s", self.
hosthost)
347 except (ConnectionClosed, OSError):
352 """Send a list of keys using legacy protocol."""
358 await asyncio.sleep(KEY_PRESS_TIMEOUT)
359 await self.
hasshass.async_add_executor_job(self.
_send_key_send_key, key)
362 """Send a key using legacy protocol."""
366 for _
in range(retry_count + 1):
371 except (ConnectionClosed, BrokenPipeError):
374 except (UnhandledResponse, AccessDenied):
376 LOGGER.debug(
"Failed sending command %s", key, exc_info=
True)
382 """Send power off command to remote."""
386 """Close remote object."""
390 """Close remote object."""
392 if self.
_remote_remote
is not None:
397 LOGGER.debug(
"Could not establish connection")
401 _RemoteT: (SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote),
402 _CommandT: (SamsungTVCommand, SamsungTVEncryptedCommand),
404 """The Bridge for WebSocket TVs (v1/v2)."""
411 port: int |
None =
None,
413 """Initialize Bridge."""
414 super().
__init__(hass, method, host, port)
415 self._remote: _RemoteT |
None =
None
416 self._remote_lock = asyncio.Lock()
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()
426 """Send the commands using websocket protocol."""
430 for _
in range(retry_count + 1):
432 if remote := await self._async_get_remote():
433 await remote.send_commands(commands)
447 """Create or return a remote control instance."""
448 if (remote := self._remote)
and remote.is_alive():
452 async
with self._remote_lock:
455 return await self._async_get_remote_under_lock()
459 """Create or return a remote control instance."""
462 """Close remote object."""
464 if self._remote
is not None:
466 await self._remote.close()
468 except OSError
as err:
469 LOGGER.debug(
"Error closing connection to %s: %s", self.host, err)
473 SamsungTVWSBaseBridge[SamsungTVWSAsyncRemote, SamsungTVCommand]
475 """The Bridge for WebSocket TVs (v2)."""
482 port: int |
None =
None,
483 entry_data: Mapping[str, Any] |
None =
None,
485 """Initialize Bridge."""
486 super().
__init__(hass, method, host, port)
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
493 """Check if a flag exists in latest device info."""
494 if not ((info := self.
_device_info_device_info)
and (device := info.get(
"device"))):
496 return device.get(key)
499 """Tells if the TV is on."""
504 LOGGER.debug(
"Checking if TV %s is on using device info", self.host)
507 return info
is not None and info[
"device"][
"PowerState"] ==
"on"
512 """Try to connect to the Websocket TV."""
513 for self.port
in WEBSOCKET_PORTS:
515 CONF_NAME: VALUE_CONF_NAME,
516 CONF_HOST: self.host,
517 CONF_METHOD: self.method,
518 CONF_PORT: self.port,
521 CONF_TIMEOUT: TIMEOUT_REQUEST,
526 LOGGER.debug(
"Try config: %s", config)
527 async
with SamsungTVWSAsyncRemote(
530 token=self.
tokentoken,
531 timeout=TIMEOUT_REQUEST,
532 name=VALUE_CONF_NAME,
535 self.
tokentoken = remote.token
536 LOGGER.debug(
"Working config: %s", config)
537 return RESULT_SUCCESS
538 except ConnectionClosedError
as err:
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"
548 result = RESULT_NOT_SUPPORTED
549 except WebSocketException
as err:
551 "Working but unsupported config: %s, error: %s", config, err
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)
563 return RESULT_CANNOT_CONNECT
566 """Try to gather infos of this TV."""
567 if self._rest_api
is None:
569 rest_api = SamsungTVAsyncRest(
573 timeout=TIMEOUT_WEBSOCKET,
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)
585 """Send the launch_app command using websocket protocol."""
586 await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)])
589 """Get installed app list."""
590 await self._async_send_commands([ChannelEmitCommand.get_installed_app()])
593 """Send a list of keys using websocket protocol."""
594 await self._async_send_commands([SendRemoteKey.click(key)
for key
in keys])
597 """Create or return a remote control instance."""
598 if self.
_remote_remote
is None or not self.
_remote_remote.is_alive():
600 LOGGER.debug(
"Create SamsungTVWSBridge for %s", self.host)
605 token=self.
tokentoken,
606 timeout=TIMEOUT_WEBSOCKET,
607 name=VALUE_CONF_NAME,
611 except UnauthorizedError
as err:
613 "Failed to get remote for %s, re-authentication required: %s",
618 self._notify_reauth_callback()
620 except ConnectionClosedError
as err:
622 "Failed to get remote for %s: %s",
627 except ConnectionFailure
as err:
630 "Unexpected ConnectionFailure trying to get remote for %s, "
631 "please report this issue: %s"
637 except (WebSocketException, AsyncioTimeoutError, OSError)
as err:
638 LOGGER.debug(
"Failed to get remote for %s: %s", self.host, repr(err))
641 LOGGER.debug(
"Created SamsungTVWSBridge for %s", self.host)
647 "SamsungTVWSBridge has provided a new token %s",
651 self._notify_update_config_entry({CONF_TOKEN: self.
tokentoken})
655 """Received event from remote websocket."""
656 if event == ED_INSTALLED_APP_EVENT:
657 self._notify_app_list_callback(
659 app[
"name"]: app[
"appId"]
661 parse_installed_app(response),
662 key=
lambda app: cast(str, app[
"name"]),
667 if event == MS_ERROR_EVENT:
670 if (data := response.get(
"data"))
and (
671 message := data.get(
"message")
672 ) ==
"unrecognized method value : ms.remote.control":
675 "Your TV seems to be unsupported by SamsungTVWSBridge"
676 " and needs a PIN: '%s'. Updating config entry"
680 self._notify_update_config_entry(
682 CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET,
683 CONF_PORT: ENCRYPTED_WEBSOCKET_PORT,
688 """Send power off command to remote."""
690 await self._async_send_commands(SendRemoteKey.hold(
"KEY_POWER", 3))
692 await self._async_send_commands([SendRemoteKey.click(
"KEY_POWER")])
696 SamsungTVWSBaseBridge[SamsungTVEncryptedWSAsyncRemote, SamsungTVEncryptedCommand]
698 """The Bridge for Encrypted WebSocket TVs (v1 - J/H models)."""
705 port: int |
None =
None,
706 entry_data: Mapping[str, Any] |
None =
None,
708 """Initialize Bridge."""
709 super().
__init__(hass, method, host, port)
711 self.
_model_model: str |
None =
None
714 self.
tokentoken = entry_data.get(CONF_TOKEN)
716 self.
_model_model = entry_data.get(CONF_MODEL)
721 self.
_device_info_device_info: dict[str, Any] |
None =
None
724 """Try to connect to the Websocket TV."""
725 self.
portport = ENCRYPTED_WEBSOCKET_PORT
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,
735 LOGGER.debug(
"Try config: %s", config)
736 async
with SamsungTVEncryptedWSAsyncRemote(
740 token=self.
tokentoken
or "",
742 timeout=TIMEOUT_REQUEST,
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)
751 LOGGER.debug(
"Working config: %s", config)
752 return RESULT_SUCCESS
754 return RESULT_CANNOT_CONNECT
757 """Try to gather infos of this TV."""
759 rest_api_ports: Iterable[int] = WEBSOCKET_PORTS
764 for rest_api_port
in rest_api_ports:
766 rest_api = SamsungTVAsyncRest(
770 timeout=TIMEOUT_WEBSOCKET,
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)
783 """Send a list of keys using websocket protocol."""
784 await self._async_send_commands(
785 [SendEncryptedRemoteKey.click(key)
for key
in keys]
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():
794 LOGGER.debug(
"Create SamsungTVEncryptedBridge for %s", self.host)
796 self.
_remote_remote = SamsungTVEncryptedWSAsyncRemote(
800 token=self.
tokentoken
or "",
802 timeout=TIMEOUT_WEBSOCKET,
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))
810 LOGGER.debug(
"Created SamsungTVEncryptedBridge for %s", self.host)
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"))
824 "Unknown power_off command for %s (%s): sending KEY_POWEROFF"
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_reauth_callback(self)
None _notify_update_config_entry(self, Mapping[str, Any] updates)
None async_power_off(self)
None register_update_config_entry_callback(self, Callable[[Mapping[str, Any]], None] func)
SamsungTVBridge get_bridge(HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
None async_close_remote(self)
None _async_send_power_off(self)
dict[str, Any]|None async_device_info(self)
None async_send_keys(self, list[str] keys)
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None)
None register_reauth_callback(self, CALLBACK_TYPE func)
None async_request_app_list(self)
str async_try_connect(self)
None register_app_list_callback(self, Callable[[dict[str, str]], None] func)
None _notify_app_list_callback(self, dict[str, str] app_list)
bool power_off_in_progress(self)
None async_send_keys(self, list[str] keys)
dict[str, Any]|None async_device_info(self)
_power_off_warning_logged
SamsungTVEncryptedWSAsyncRemote|None _async_get_remote_under_lock(self)
None _async_send_power_off(self)
str async_try_connect(self)
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
None async_send_keys(self, list[str] keys)
None _send_key(self, str key)
None _notify_reauth_callback(self)
None async_close_remote(self)
None __init__(self, HomeAssistant hass, str method, str host, int|None port)
str async_try_connect(self)
None _async_send_power_off(self)
dict[str, Any]|None async_device_info(self)
SamsungTVWSAsyncRemote|None _async_get_remote_under_lock(self)
Any|None _get_device_spec(self, str key)
None async_request_app_list(self)
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None, Mapping[str, Any]|None entry_data=None)
None async_send_keys(self, list[str] keys)
None _async_send_power_off(self)
None _remote_event(self, str event, Any response)
str async_try_connect(self)
dict[str, Any]|None async_device_info(self, bool force=False)
None async_launch_app(self, str app_id)
web.Response get(self, web.Request request, str config_key)
tuple[str, int|None, str|None, dict[str, Any]|None] async_get_device_info(HomeAssistant hass, str host)
None _async_send_commands(self, list[_CommandT] commands)
_RemoteT|None _async_get_remote(self)
_RemoteT|None _async_get_remote_under_lock(self)
None async_close_remote(self)
None __init__(self, HomeAssistant hass, str method, str host, int|None port=None)
str|None mac_from_device_info(dict[str, Any] info)
bool model_requires_encryption(str|None model)
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)