Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Samsung TV integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Coroutine, Mapping
6 from functools import partial
7 from typing import Any
8 from urllib.parse import urlparse
9 
10 import getmac
11 
12 from homeassistant.components import ssdp
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import (
15  CONF_HOST,
16  CONF_MAC,
17  CONF_METHOD,
18  CONF_MODEL,
19  CONF_PORT,
20  CONF_TOKEN,
21  EVENT_HOMEASSISTANT_STOP,
22  Platform,
23 )
24 from homeassistant.core import Event, HomeAssistant, callback
25 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
26 from homeassistant.helpers import device_registry as dr, entity_registry as er
27 from homeassistant.helpers.debounce import Debouncer
28 
29 from .bridge import (
30  SamsungTVBridge,
31  async_get_device_info,
32  mac_from_device_info,
33  model_requires_encryption,
34 )
35 from .const import (
36  CONF_SESSION_ID,
37  CONF_SSDP_MAIN_TV_AGENT_LOCATION,
38  CONF_SSDP_RENDERING_CONTROL_LOCATION,
39  ENTRY_RELOAD_COOLDOWN,
40  LEGACY_PORT,
41  LOGGER,
42  METHOD_ENCRYPTED_WEBSOCKET,
43  METHOD_LEGACY,
44  UPNP_SVC_MAIN_TV_AGENT,
45  UPNP_SVC_RENDERING_CONTROL,
46 )
47 from .coordinator import SamsungTVDataUpdateCoordinator
48 
49 PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
50 
51 
52 SamsungTVConfigEntry = ConfigEntry[SamsungTVDataUpdateCoordinator]
53 
54 
55 @callback
57  hass: HomeAssistant, data: dict[str, Any]
58 ) -> SamsungTVBridge:
59  """Get device bridge."""
60  return SamsungTVBridge.get_bridge(
61  hass,
62  data[CONF_METHOD],
63  data[CONF_HOST],
64  data[CONF_PORT],
65  data,
66  )
67 
68 
70  """Reload only after the timer expires."""
71 
72  def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
73  """Init the debounced entry reloader."""
74  self.hasshass = hass
75  self.entryentry = entry
76  self.tokentoken = self.entryentry.data.get(CONF_TOKEN)
77  self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer(
78  hass,
79  LOGGER,
80  cooldown=ENTRY_RELOAD_COOLDOWN,
81  immediate=False,
82  function=self._async_reload_entry_async_reload_entry,
83  )
84 
85  async def async_call(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
86  """Start the countdown for a reload."""
87  if (new_token := entry.data.get(CONF_TOKEN)) != self.tokentoken:
88  LOGGER.debug("Skipping reload as its a token update")
89  self.tokentoken = new_token
90  return # Token updates should not trigger a reload
91  LOGGER.debug("Calling debouncer to get a reload after cooldown")
92  await self._debounced_reload.async_call()
93 
94  @callback
95  def async_shutdown(self) -> None:
96  """Cancel any pending reload."""
97  self._debounced_reload.async_shutdown()
98 
99  async def _async_reload_entry(self) -> None:
100  """Reload entry."""
101  LOGGER.debug("Reloading entry %s", self.entryentry.title)
102  await self.hasshass.config_entries.async_reload(self.entryentry.entry_id)
103 
104 
105 async def _async_update_ssdp_locations(hass: HomeAssistant, entry: ConfigEntry) -> None:
106  """Update ssdp locations from discovery cache."""
107  updates = {}
108  for ssdp_st, key in (
109  (UPNP_SVC_RENDERING_CONTROL, CONF_SSDP_RENDERING_CONTROL_LOCATION),
110  (UPNP_SVC_MAIN_TV_AGENT, CONF_SSDP_MAIN_TV_AGENT_LOCATION),
111  ):
112  for discovery_info in await ssdp.async_get_discovery_info_by_st(hass, ssdp_st):
113  location = discovery_info.ssdp_location
114  host = urlparse(location).hostname
115  if host == entry.data[CONF_HOST]:
116  updates[key] = location
117  break
118 
119  if updates:
120  hass.config_entries.async_update_entry(entry, data={**entry.data, **updates})
121 
122 
123 async def async_setup_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) -> bool:
124  """Set up the Samsung TV platform."""
125  # Initialize bridge
126  if entry.data.get(CONF_METHOD) == METHOD_ENCRYPTED_WEBSOCKET:
127  if not entry.data.get(CONF_TOKEN) or not entry.data.get(CONF_SESSION_ID):
128  raise ConfigEntryAuthFailed(
129  "Token and session id are required in encrypted mode"
130  )
131  bridge = await _async_create_bridge_with_updated_data(hass, entry)
132 
133  @callback
134  def _access_denied() -> None:
135  """Access denied callback."""
136  LOGGER.debug("Access denied in getting remote object")
137  entry.async_start_reauth(hass)
138 
139  bridge.register_reauth_callback(_access_denied)
140 
141  # Ensure updates get saved against the config_entry
142  @callback
143  def _update_config_entry(updates: Mapping[str, Any]) -> None:
144  """Update config entry with the new token."""
145  hass.config_entries.async_update_entry(entry, data={**entry.data, **updates})
146 
147  bridge.register_update_config_entry_callback(_update_config_entry)
148 
149  async def stop_bridge(event: Event | None = None) -> None:
150  """Stop SamsungTV bridge connection."""
151  LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host)
152  await bridge.async_close_remote()
153 
154  entry.async_on_unload(
155  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
156  )
157  entry.async_on_unload(stop_bridge)
158 
159  await _async_update_ssdp_locations(hass, entry)
160 
161  # We must not await after we setup the reload or there
162  # will be a race where the config flow will see the entry
163  # as not loaded and may reload it
164  debounced_reloader = DebouncedEntryReloader(hass, entry)
165  entry.async_on_unload(debounced_reloader.async_shutdown)
166  entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call))
167 
168  coordinator = SamsungTVDataUpdateCoordinator(hass, bridge)
169  await coordinator.async_config_entry_first_refresh()
170  entry.runtime_data = coordinator
171  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
172 
173  return True
174 
175 
177  hass: HomeAssistant, entry: ConfigEntry
178 ) -> SamsungTVBridge:
179  """Create a bridge object and update any missing data in the config entry."""
180  updated_data: dict[str, str | int] = {}
181  host: str = entry.data[CONF_HOST]
182  port: int | None = entry.data.get(CONF_PORT)
183  method: str | None = entry.data.get(CONF_METHOD)
184  load_info_attempted = False
185  info: dict[str, Any] | None = None
186 
187  if not port or not method:
188  LOGGER.debug("Attempting to get port or method for %s", host)
189  if method == METHOD_LEGACY:
190  port = LEGACY_PORT
191  else:
192  # When we imported from yaml we didn't setup the method
193  # because we didn't know it
194  _result, port, method, info = await async_get_device_info(hass, host)
195  load_info_attempted = True
196  if not port or not method:
197  raise ConfigEntryNotReady(
198  "Failed to determine connection method, make sure the device is on."
199  )
200 
201  LOGGER.debug("Updated port to %s and method to %s for %s", port, method, host)
202  updated_data[CONF_PORT] = port
203  updated_data[CONF_METHOD] = method
204 
205  bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data})
206 
207  mac: str | None = entry.data.get(CONF_MAC)
208  model: str | None = entry.data.get(CONF_MODEL)
209  mac_is_incorrectly_formatted = mac and dr.format_mac(mac) != mac
210  if (
211  not mac or not model or mac_is_incorrectly_formatted
212  ) and not load_info_attempted:
213  info = await bridge.async_device_info()
214 
215  if not mac or mac_is_incorrectly_formatted:
216  LOGGER.debug("Attempting to get mac for %s", host)
217  if info:
218  mac = mac_from_device_info(info)
219 
220  if not mac:
221  mac = await hass.async_add_executor_job(
222  partial(getmac.get_mac_address, ip=host)
223  )
224 
225  if mac and mac != "none":
226  # Samsung sometimes returns a value of "none" for the mac address
227  # this should be ignored
228  LOGGER.debug("Updated mac to %s for %s", mac, host)
229  updated_data[CONF_MAC] = dr.format_mac(mac)
230  else:
231  LOGGER.warning("Failed to get mac for %s", host)
232 
233  if not model:
234  LOGGER.debug("Attempting to get model for %s", host)
235  if info:
236  model = info.get("device", {}).get("modelName")
237  if model:
238  LOGGER.debug("Updated model to %s for %s", model, host)
239  updated_data[CONF_MODEL] = model
240 
241  if model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET:
242  LOGGER.debug(
243  (
244  "Detected model %s for %s. Some televisions from H and J series use "
245  "an encrypted protocol but you are using %s which may not be supported"
246  ),
247  model,
248  host,
249  method,
250  )
251 
252  if updated_data:
253  data = {**entry.data, **updated_data}
254  hass.config_entries.async_update_entry(entry, data=data)
255 
256  return bridge
257 
258 
259 async def async_unload_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) -> bool:
260  """Unload a config entry."""
261  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
262 
263 
264 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
265  """Migrate old entry."""
266  version = config_entry.version
267  minor_version = config_entry.minor_version
268 
269  LOGGER.debug("Migrating from version %s.%s", version, minor_version)
270 
271  # 1 -> 2: Unique ID format changed, so delete and re-import:
272  if version == 1:
273  dev_reg = dr.async_get(hass)
274  dev_reg.async_clear_config_entry(config_entry.entry_id)
275 
276  en_reg = er.async_get(hass)
277  en_reg.async_clear_config_entry(config_entry.entry_id)
278 
279  version = 2
280  hass.config_entries.async_update_entry(config_entry, version=2)
281 
282  if version == 2:
283  if minor_version < 2:
284  # Cleanup invalid MAC addresses - see #103512
285  # Reverted due to device registry collisions - see #119082 / #119249
286 
287  minor_version = 2
288  hass.config_entries.async_update_entry(config_entry, minor_version=2)
289 
290  LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
291 
292  return True
None __init__(self, HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:72
None async_call(self, HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:85
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
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
SamsungTVBridge _async_create_bridge_with_updated_data(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:178
bool async_setup_entry(HomeAssistant hass, SamsungTVConfigEntry entry)
Definition: __init__.py:123
None _async_update_ssdp_locations(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:105
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:264
SamsungTVBridge _async_get_device_bridge(HomeAssistant hass, dict[str, Any] data)
Definition: __init__.py:58
bool async_unload_entry(HomeAssistant hass, SamsungTVConfigEntry entry)
Definition: __init__.py:259