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