Add audio output select to Cambridge Audio (#129366)

This commit is contained in:
Noah Husby 2024-10-30 09:28:01 -04:00 committed by GitHub
parent 6c047e2678
commit 0cd5deaa3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 136 additions and 2 deletions

View File

@ -8,6 +8,9 @@
"dim": "mdi:brightness-6",
"off": "mdi:brightness-3"
}
},
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"
}
},
"switch": {

View File

@ -1,7 +1,7 @@
"""Support for Cambridge Audio select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from dataclasses import dataclass, field
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import DisplayBrightness
@ -19,10 +19,34 @@ from .entity import CambridgeAudioEntity
class CambridgeAudioSelectEntityDescription(SelectEntityDescription):
"""Describes Cambridge Audio select entity."""
options_fn: Callable[[StreamMagicClient], list[str]] = field(default=lambda _: [])
load_fn: Callable[[StreamMagicClient], bool] = field(default=lambda _: True)
value_fn: Callable[[StreamMagicClient], str | None]
set_value_fn: Callable[[StreamMagicClient, str], Awaitable[None]]
async def _audio_output_set_value_fn(client: StreamMagicClient, value: str) -> None:
"""Set the audio output using the display name."""
audio_output_id = next(
(output.id for output in client.audio_output.outputs if value == output.name),
None,
)
assert audio_output_id is not None
await client.set_audio_output(audio_output_id)
def _audio_output_value_fn(client: StreamMagicClient) -> str | None:
"""Convert the current audio output id to name."""
return next(
(
output.name
for output in client.audio_output.outputs
if client.state.audio_output == output.id
),
None,
)
CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
CambridgeAudioSelectEntityDescription(
key="display_brightness",
@ -34,6 +58,17 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
DisplayBrightness(value)
),
),
CambridgeAudioSelectEntityDescription(
key="audio_output",
translation_key="audio_output",
entity_category=EntityCategory.CONFIG,
options_fn=lambda client: [
output.name for output in client.audio_output.outputs
],
load_fn=lambda client: len(client.audio_output.outputs) > 0,
value_fn=_audio_output_value_fn,
set_value_fn=_audio_output_set_value_fn,
),
)
@ -46,7 +81,9 @@ async def async_setup_entry(
client: StreamMagicClient = entry.runtime_data
entities: list[CambridgeAudioSelect] = [
CambridgeAudioSelect(client, description) for description in CONTROL_ENTITIES
CambridgeAudioSelect(client, description)
for description in CONTROL_ENTITIES
if description.load_fn(client)
]
async_add_entities(entities)
@ -65,6 +102,9 @@ class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
super().__init__(client)
self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
options_fn = description.options_fn(client)
if options_fn:
self._attr_options = options_fn
@property
def current_option(self) -> str | None:

View File

@ -32,6 +32,9 @@
"dim": "Dim",
"off": "[%key:common::state::off%]"
}
},
"audio_output": {
"name": "Audio output"
}
},
"switch": {

View File

@ -4,6 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, Mock, patch
from aiostreammagic.models import (
AudioOutput,
Display,
Info,
NowPlaying,
@ -63,6 +64,9 @@ def mock_stream_magic_client() -> Generator[AsyncMock]:
client.preset_list = PresetList.from_json(
load_fixture("get_presets_list.json", DOMAIN)
)
client.audio_output = AudioOutput.from_json(
load_fixture("get_audio_output.json", DOMAIN)
)
client.is_connected = Mock(return_value=True)
client.position_last_updated = client.play_state.position
client.unregister_state_update_callbacks.return_value = True

View File

@ -0,0 +1,16 @@
{
"outputs": [
{
"id": "speaker_a",
"name": "Speaker A"
},
{
"id": "speaker_b",
"name": "Speaker B"
},
{
"id": "headphones",
"name": "Headphones"
}
]
}

View File

@ -1,4 +1,61 @@
# serializer version: 1
# name: test_all_entities[select.cambridge_audio_cxnv2_audio_output-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Speaker A',
'Speaker B',
'Headphones',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.cambridge_audio_cxnv2_audio_output',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Audio output',
'platform': 'cambridge_audio',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'audio_output',
'unique_id': '0020c2d8-audio_output',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[select.cambridge_audio_cxnv2_audio_output-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Cambridge Audio CXNv2 Audio output',
'options': list([
'Speaker A',
'Speaker B',
'Headphones',
]),
}),
'context': <ANY>,
'entity_id': 'select.cambridge_audio_cxnv2_audio_output',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[select.cambridge_audio_cxnv2_display_brightness-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -51,3 +51,14 @@ async def test_setting_value(
blocking=True,
)
mock_stream_magic_client.set_display_brightness.assert_called_once_with("dim")
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.cambridge_audio_cxnv2_audio_output",
ATTR_OPTION: "Speaker A",
},
blocking=True,
)
mock_stream_magic_client.set_audio_output.assert_called_once_with("speaker_a")