Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helper methods for common tasks."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 from typing import TYPE_CHECKING, Any, Concatenate, overload
8 
9 from requests.exceptions import Timeout
10 from soco import SoCo
11 from soco.exceptions import SoCoException, SoCoUPnPException
12 
13 from homeassistant.helpers.dispatcher import dispatcher_send
14 
15 from .const import SONOS_SPEAKER_ACTIVITY
16 from .exception import SonosUpdateError
17 
18 if TYPE_CHECKING:
19  from .entity import SonosEntity
20  from .household_coordinator import SonosHouseholdCoordinator
21  from .media import SonosMedia
22  from .speaker import SonosSpeaker
23 
24 UID_PREFIX = "RINCON_"
25 UID_POSTFIX = "01400"
26 
27 _LOGGER = logging.getLogger(__name__)
28 
29 type _SonosEntitiesType = (
30  SonosSpeaker | SonosMedia | SonosEntity | SonosHouseholdCoordinator
31 )
32 type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], _R]
33 type _ReturnFuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], _R | None]
34 
35 
36 @overload
37 def soco_error[_T: _SonosEntitiesType, **_P, _R](
38  errorcodes: None = ...,
39 ) -> Callable[[_FuncType[_T, _P, _R]], _FuncType[_T, _P, _R]]: ...
40 
41 
42 @overload
43 def soco_error[_T: _SonosEntitiesType, **_P, _R](
44  errorcodes: list[str],
45 ) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: ...
46 
47 
48 def soco_error[_T: _SonosEntitiesType, **_P, _R](
49  errorcodes: list[str] | None = None,
50 ) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]:
51  """Filter out specified UPnP errors and raise exceptions for service calls."""
52 
53  def decorator(funct: _FuncType[_T, _P, _R]) -> _ReturnFuncType[_T, _P, _R]:
54  """Decorate functions."""
55 
56  def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None:
57  """Wrap for all soco UPnP exception."""
58  args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None)
59  try:
60  result = funct(self, *args, **kwargs)
61  except (OSError, SoCoException, SoCoUPnPException, Timeout) as err:
62  error_code = getattr(err, "error_code", None)
63  function = funct.__qualname__
64  if errorcodes and error_code in errorcodes:
65  _LOGGER.debug(
66  "Error code %s ignored in call to %s", error_code, function
67  )
68  return None
69 
70  if (target := _find_target_identifier(self, args_soco)) is None:
71  raise RuntimeError("Unexpected use of soco_error") from err
72 
73  message = f"Error calling {function} on {target}: {err}"
74  raise SonosUpdateError(message) from err
75 
76  dispatch_soco = args_soco or self.soco # type: ignore[union-attr]
78  self.hass,
79  f"{SONOS_SPEAKER_ACTIVITY}-{dispatch_soco.uid}",
80  funct.__qualname__,
81  )
82  return result
83 
84  return wrapper
85 
86  return decorator
87 
88 
89 def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None:
90  """Extract the best available target identifier from the provided instance object."""
91  if entity_id := getattr(instance, "entity_id", None):
92  # SonosEntity instance
93  return entity_id
94  if zone_name := getattr(instance, "zone_name", None):
95  # SonosSpeaker instance
96  return zone_name
97  if speaker := getattr(instance, "speaker", None):
98  # Holds a SonosSpeaker instance attribute
99  return speaker.zone_name
100  if soco := getattr(instance, "soco", fallback_soco):
101  # Holds a SoCo instance attribute
102  # Only use attributes with no I/O
103  return soco._player_name or soco.ip_address # noqa: SLF001
104  return None
105 
106 
107 def hostname_to_uid(hostname: str) -> str:
108  """Convert a Sonos hostname to a uid."""
109  if hostname.startswith("Sonos-"):
110  baseuid = hostname.removeprefix("Sonos-").replace(".local.", "")
111  elif hostname.startswith("sonos"):
112  baseuid = hostname.removeprefix("sonos").replace(".local.", "")
113  else:
114  raise ValueError(f"{hostname} is not a sonos device.")
115  return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}"
116 
117 
118 def sync_get_visible_zones(soco: SoCo) -> set[SoCo]:
119  """Ensure I/O attributes are cached and return visible zones."""
120  _ = soco.household_id
121  _ = soco.uid
122  return soco.visible_zones
str|None _find_target_identifier(Any instance, SoCo|None fallback_soco)
Definition: helpers.py:89
set[SoCo] sync_get_visible_zones(SoCo soco)
Definition: helpers.py:118
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137