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", "dim": "mdi:brightness-6",
"off": "mdi:brightness-3" "off": "mdi:brightness-3"
} }
},
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"
} }
}, },
"switch": { "switch": {

View File

@ -1,7 +1,7 @@
"""Support for Cambridge Audio select entities.""" """Support for Cambridge Audio select entities."""
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass, field
from aiostreammagic import StreamMagicClient from aiostreammagic import StreamMagicClient
from aiostreammagic.models import DisplayBrightness from aiostreammagic.models import DisplayBrightness
@ -19,10 +19,34 @@ from .entity import CambridgeAudioEntity
class CambridgeAudioSelectEntityDescription(SelectEntityDescription): class CambridgeAudioSelectEntityDescription(SelectEntityDescription):
"""Describes Cambridge Audio select entity.""" """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] value_fn: Callable[[StreamMagicClient], str | None]
set_value_fn: Callable[[StreamMagicClient, str], Awaitable[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, ...] = ( CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
CambridgeAudioSelectEntityDescription( CambridgeAudioSelectEntityDescription(
key="display_brightness", key="display_brightness",
@ -34,6 +58,17 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
DisplayBrightness(value) 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 client: StreamMagicClient = entry.runtime_data
entities: list[CambridgeAudioSelect] = [ 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) async_add_entities(entities)
@ -65,6 +102,9 @@ class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
super().__init__(client) super().__init__(client)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}" 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 @property
def current_option(self) -> str | None: def current_option(self) -> str | None:

View File

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

View File

@ -4,6 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aiostreammagic.models import ( from aiostreammagic.models import (
AudioOutput,
Display, Display,
Info, Info,
NowPlaying, NowPlaying,
@ -63,6 +64,9 @@ def mock_stream_magic_client() -> Generator[AsyncMock]:
client.preset_list = PresetList.from_json( client.preset_list = PresetList.from_json(
load_fixture("get_presets_list.json", DOMAIN) 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.is_connected = Mock(return_value=True)
client.position_last_updated = client.play_state.position client.position_last_updated = client.play_state.position
client.unregister_state_update_callbacks.return_value = True 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 # 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] # name: test_all_entities[select.cambridge_audio_cxnv2_display_brightness-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -51,3 +51,14 @@ async def test_setting_value(
blocking=True, blocking=True,
) )
mock_stream_magic_client.set_display_brightness.assert_called_once_with("dim") 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")