Skip to content

Commit 286d99b

Browse files
Solmathnoahhusby
andauthored
Add new audio endpoint (noahhusby#165)
Co-authored-by: Noah Husby <32528627+noahhusby@users.noreply.github.com>
1 parent 44ad28e commit 286d99b

5 files changed

Lines changed: 329 additions & 102 deletions

File tree

aiostreammagic/endpoints.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
PLAY_CONTROL = "/zone/play_control"
1111
STREAM_RADIO = "/stream/radio"
1212
POWER = "/system/power"
13+
AUDIO = "/zone/audio"
1314
ZONE_AUDIO_OUTPUT = "/zone/audio/output"
1415
DISPLAY = "/system/display"
1516
PRESET_LIST = "/presets/list"

aiostreammagic/models.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,32 @@ class ControlBusMode(StrEnum):
6363
RECEIVER = "receiver"
6464
OFF = "off"
6565

66+
6667
class StandbyMode(StrEnum):
6768
"""Standby mode"""
6869

6970
ECO = "ECO_MODE"
7071
NETWORK = "NETWORK"
7172

7273

74+
class EQFilterType(StrEnum):
75+
"""EQ filter type."""
76+
77+
LOWSHELF = "LOWSHELF"
78+
PEAKING = "PEAKING"
79+
HIGHSHELF = "HIGHSHELF"
80+
LOWPASS = "LOWPASS"
81+
HIGHPASS = "HIGHPASS"
82+
NOTCH = "NOTCH"
83+
84+
85+
class Pipeline(StrEnum):
86+
"""Pipeline type."""
87+
88+
DSP = "DSP"
89+
DIRECT = "DIRECT"
90+
91+
7392
@dataclass
7493
class Info(DataClassORJSONMixin):
7594
"""Cambridge Audio device metadata."""
@@ -268,3 +287,59 @@ class Update(DataClassORJSONMixin):
268287
metadata=field_options(alias="update_available"), default=False
269288
)
270289
updating: bool = field(metadata=field_options(alias="updating"), default=False)
290+
291+
292+
@dataclass
293+
class EQBand(DataClassORJSONMixin):
294+
"""Represents a single EQ band."""
295+
296+
index: int = field(metadata=field_options(alias="index"))
297+
filter: EQFilterType = field(metadata=field_options(alias="filter"))
298+
freq: int = field(metadata=field_options(alias="freq"))
299+
gain: float = field(metadata=field_options(alias="gain"))
300+
q: float = field(metadata=field_options(alias="q"))
301+
302+
303+
@dataclass
304+
class UserEQ(DataClassORJSONMixin):
305+
"""Represents user EQ settings."""
306+
307+
enabled: bool = field(metadata=field_options(alias="enabled"))
308+
bands: list[EQBand] = field(
309+
metadata=field_options(alias="bands"), default_factory=list
310+
)
311+
312+
313+
@dataclass
314+
class TiltEQ(DataClassORJSONMixin):
315+
"""Represents tilt EQ settings."""
316+
317+
enabled: bool = field(metadata=field_options(alias="enabled"))
318+
intensity: int = field(metadata=field_options(alias="intensity"))
319+
320+
321+
@dataclass
322+
class Audio(DataClassORJSONMixin):
323+
"""Represents audio settings including EQ and balance."""
324+
325+
digital_filter: Optional[str] = field(
326+
metadata=field_options(alias="digital_filter"), default=None
327+
)
328+
phase_invert: Optional[bool] = field(
329+
metadata=field_options(alias="phase_invert"), default=None
330+
)
331+
volume_limit_percent: Optional[int] = field(
332+
metadata=field_options(alias="volume_limit_percent"), default=None
333+
)
334+
tilt_eq: Optional[TiltEQ] = field(
335+
metadata=field_options(alias="tilt_eq"), default=None
336+
)
337+
user_eq: Optional[UserEQ] = field(
338+
metadata=field_options(alias="user_eq"), default=None
339+
)
340+
balance: Optional[int] = field(
341+
metadata=field_options(alias="balance"), default=None
342+
)
343+
pipeline: Optional[Pipeline] = field(
344+
metadata=field_options(alias="pipeline"), default=None
345+
)

aiostreammagic/stream_magic.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@
2525
PresetList,
2626
ControlBusMode,
2727
StandbyMode,
28+
Audio,
2829
)
2930
from . import endpoints as ep
3031
from .const import _LOGGER
3132

32-
VERSION = "1.0.0"
33-
3433

3534
class StreamMagicClient:
3635
"""Client for handling connections with StreamMagic enabled devices."""
@@ -51,6 +50,7 @@ def __init__(self, host: str, session: ClientSession | None = None) -> None:
5150
self._state: Optional[State] = None
5251
self._play_state: Optional[PlayState] = None
5352
self._now_playing: Optional[NowPlaying] = None
53+
self._audio: Audio | None = None
5454
self._audio_output: Optional[AudioOutput] = None
5555
self._display: Optional[Display] = None
5656
self._update: Optional[Update] = None
@@ -167,6 +167,7 @@ async def _connect_handler(self, res: Future[bool]) -> None:
167167
x = asyncio.create_task(
168168
self.consumer_handler(ws, self._subscriptions, self.futures)
169169
)
170+
170171
# mypy/typeshed bug: https://github.com/python/mypy/issues/17030
171172
# The following ignore is safe because we know the return types.
172173
(
@@ -175,6 +176,7 @@ async def _connect_handler(self, res: Future[bool]) -> None:
175176
self._state,
176177
self._play_state,
177178
self._now_playing,
179+
self._audio,
178180
self._audio_output,
179181
self._display,
180182
self._update,
@@ -185,11 +187,13 @@ async def _connect_handler(self, res: Future[bool]) -> None:
185187
self.get_state(),
186188
self.get_play_state(),
187189
self.get_now_playing(),
190+
self.get_audio(),
188191
self.get_audio_output(),
189192
self.get_display(),
190193
self.get_update(),
191194
self.get_preset_list(),
192195
)
196+
193197
subscribe_state_updates = {
194198
self.subscribe(self._async_handle_info, ep.INFO),
195199
self.subscribe(self._async_handle_sources, ep.SOURCES),
@@ -201,7 +205,9 @@ async def _connect_handler(self, res: Future[bool]) -> None:
201205
self.subscribe(self._async_handle_display, ep.DISPLAY),
202206
self.subscribe(self._async_handle_update, ep.UPDATE),
203207
self.subscribe(self._async_handle_preset_list, ep.PRESET_LIST),
208+
self.subscribe(self._async_handle_audio, ep.AUDIO),
204209
}
210+
205211
subscribe_tasks = set()
206212
for state_update in subscribe_state_updates:
207213
subscribe_tasks.add(asyncio.create_task(state_update))
@@ -339,6 +345,13 @@ def now_playing(self) -> NowPlaying:
339345
raise StreamMagicError("NowPlaying not available.")
340346
return self._now_playing
341347

348+
@property
349+
def audio(self) -> Audio:
350+
"""Return a type-guaranteed instance of Audio"""
351+
if not self._audio:
352+
raise StreamMagicError("Audio not available.")
353+
return self._audio
354+
342355
@property
343356
def audio_output(self) -> AudioOutput:
344357
"""Return a type-guaranteed instance of AudioOutput"""
@@ -393,6 +406,11 @@ async def get_now_playing(self) -> NowPlaying:
393406
data = await self.request(ep.NOW_PLAYING)
394407
return NowPlaying.from_dict(data["params"]["data"])
395408

409+
async def get_audio(self) -> Audio | None:
410+
"""Get audio information from device."""
411+
data = await self.request(ep.AUDIO)
412+
return Audio.from_dict(data["params"]["data"])
413+
396414
async def get_audio_output(self) -> AudioOutput:
397415
"""Get audio output information from device."""
398416
data = await self.request(ep.ZONE_AUDIO_OUTPUT)
@@ -457,6 +475,13 @@ async def _async_handle_now_playing(self, payload: dict[str, Any]) -> None:
457475
self._now_playing = NowPlaying.from_dict(params["data"])
458476
await self.do_state_update_callbacks()
459477

478+
async def _async_handle_audio(self, payload: dict[str, Any]) -> None:
479+
"""Handle async audio update."""
480+
params = payload["params"]
481+
if "data" in params:
482+
self._audio = Audio.from_dict(params["data"])
483+
await self.do_state_update_callbacks()
484+
460485
async def _async_handle_audio_output(self, payload: dict[str, Any]) -> None:
461486
"""Handle async audio output update."""
462487
params = payload["params"]
@@ -653,9 +678,14 @@ async def set_auto_power_down(self, auto_power_down_time_seconds: int) -> None:
653678
ep.POWER, params={"auto_power_down": auto_power_down_time_seconds}
654679
)
655680

656-
async def __aenter__(self):
681+
async def __aenter__(self) -> "StreamMagicClient":
657682
await self.connect()
658683
return self
659684

660-
async def __aexit__(self, exc_type, exc, tb):
685+
async def __aexit__(
686+
self,
687+
exc_type: type[BaseException] | None,
688+
exc: BaseException | None,
689+
tb: object | None,
690+
) -> None:
661691
await self.disconnect()

0 commit comments

Comments
 (0)