Expose sound cards and profiles with endpoint (#1534)
* Expose sound cards and profiles with endpoint * Fix naming * Fix issue * Update API
This commit is contained in:
parent
9393521f98
commit
6721b8f265
22
API.md
22
API.md
|
@ -863,6 +863,19 @@ return:
|
|||
"version": "1",
|
||||
"latest_version": "2",
|
||||
"audio": {
|
||||
"card": [
|
||||
{
|
||||
"name": "...",
|
||||
"description": "...",
|
||||
"profiles": [
|
||||
{
|
||||
"name": "...",
|
||||
"description": "...",
|
||||
"active": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"input": [
|
||||
{
|
||||
"name": "...",
|
||||
|
@ -931,6 +944,15 @@ return:
|
|||
}
|
||||
```
|
||||
|
||||
- POST `/audio/profile`
|
||||
|
||||
```json
|
||||
{
|
||||
"card": "...",
|
||||
"name": "..."
|
||||
}
|
||||
```
|
||||
|
||||
- GET `/audio/stats`
|
||||
|
||||
```json
|
||||
|
|
|
@ -329,6 +329,7 @@ class RestAPI(CoreSysAttributes):
|
|||
web.post("/audio/update", api_audio.update),
|
||||
web.post("/audio/restart", api_audio.restart),
|
||||
web.post("/audio/reload", api_audio.reload),
|
||||
web.post("/audio/profile", api_audio.set_profile),
|
||||
web.post("/audio/volume/{source}", api_audio.set_volume),
|
||||
web.post("/audio/default/{source}", api_audio.set_default),
|
||||
]
|
||||
|
|
|
@ -11,6 +11,7 @@ from ..const import (
|
|||
ATTR_AUDIO,
|
||||
ATTR_BLK_READ,
|
||||
ATTR_BLK_WRITE,
|
||||
ATTR_CARD,
|
||||
ATTR_CPU_PERCENT,
|
||||
ATTR_HOST,
|
||||
ATTR_INPUT,
|
||||
|
@ -44,6 +45,10 @@ SCHEMA_VOLUME = vol.Schema(
|
|||
|
||||
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
||||
|
||||
SCHEMA_PROFILE = vol.Schema(
|
||||
{vol.Required(ATTR_CARD): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str)}
|
||||
)
|
||||
|
||||
|
||||
class APIAudio(CoreSysAttributes):
|
||||
"""Handle RESTful API for Audio functions."""
|
||||
|
@ -56,13 +61,12 @@ class APIAudio(CoreSysAttributes):
|
|||
ATTR_LATEST_VERSION: self.sys_audio.latest_version,
|
||||
ATTR_HOST: str(self.sys_docker.network.audio),
|
||||
ATTR_AUDIO: {
|
||||
ATTR_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards],
|
||||
ATTR_INPUT: [
|
||||
attr.asdict(profile)
|
||||
for profile in self.sys_host.sound.input_profiles
|
||||
attr.asdict(stream) for stream in self.sys_host.sound.inputs
|
||||
],
|
||||
ATTR_OUTPUT: [
|
||||
attr.asdict(profile)
|
||||
for profile in self.sys_host.sound.output_profiles
|
||||
attr.asdict(stream) for stream in self.sys_host.sound.outputs
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -110,7 +114,7 @@ class APIAudio(CoreSysAttributes):
|
|||
|
||||
@api_process
|
||||
async def set_volume(self, request: web.Request) -> None:
|
||||
"""Set Audio information."""
|
||||
"""Set audio volume on stream."""
|
||||
source: SourceType = SourceType(request.match_info.get("source"))
|
||||
body = await api_validate(SCHEMA_VOLUME, request)
|
||||
|
||||
|
@ -120,8 +124,17 @@ class APIAudio(CoreSysAttributes):
|
|||
|
||||
@api_process
|
||||
async def set_default(self, request: web.Request) -> None:
|
||||
"""Set Audio default sources."""
|
||||
"""Set audio default stream."""
|
||||
source: SourceType = SourceType(request.match_info.get("source"))
|
||||
body = await api_validate(SCHEMA_DEFAULT, request)
|
||||
|
||||
await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME]))
|
||||
|
||||
@api_process
|
||||
async def set_profile(self, request: web.Request) -> None:
|
||||
"""Set audio default sources."""
|
||||
body = await api_validate(SCHEMA_DEFAULT, request)
|
||||
|
||||
await asyncio.shield(
|
||||
self.sys_host.sound.set_profile(body[ATTR_CARD], body[ATTR_NAME])
|
||||
)
|
||||
|
|
|
@ -42,11 +42,11 @@ class APIHardware(CoreSysAttributes):
|
|||
ATTR_AUDIO: {
|
||||
ATTR_INPUT: {
|
||||
profile.name: profile.description
|
||||
for profile in self.sys_host.sound.input_profiles
|
||||
for profile in self.sys_host.sound.inputs
|
||||
},
|
||||
ATTR_OUTPUT: {
|
||||
profile.name: profile.description
|
||||
for profile in self.sys_host.sound.output_profiles
|
||||
for profile in self.sys_host.sound.outputs
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,6 +233,7 @@ ATTR_STAGE = "stage"
|
|||
ATTR_CLI = "cli"
|
||||
ATTR_DEFAULT = "default"
|
||||
ATTR_VOLUME = "volume"
|
||||
ATTR_CARD = "card"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
|
|
|
@ -22,7 +22,7 @@ class SourceType(str, Enum):
|
|||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class AudioProfile:
|
||||
class AudioStream:
|
||||
"""Represent a input/output profile."""
|
||||
|
||||
name: str = attr.ib()
|
||||
|
@ -31,27 +31,51 @@ class AudioProfile:
|
|||
default: bool = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class SoundProfile:
|
||||
"""Represent a Sound Card profile."""
|
||||
|
||||
name: str = attr.ib()
|
||||
description: str = attr.ib()
|
||||
active: bool = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class SoundCard:
|
||||
"""Represent a Sound Card."""
|
||||
|
||||
name: str = attr.ib()
|
||||
description: str = attr.ib()
|
||||
profiles: List[SoundProfile] = attr.ib()
|
||||
|
||||
|
||||
class SoundControl(CoreSysAttributes):
|
||||
"""Pulse control from Host."""
|
||||
|
||||
def __init__(self, coresys: CoreSys) -> None:
|
||||
"""Initialize PulseAudio sound control."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self._input: List[AudioProfile] = []
|
||||
self._output: List[AudioProfile] = []
|
||||
self._cards: List[SoundCard] = []
|
||||
self._inputs: List[AudioStream] = []
|
||||
self._outputs: List[AudioStream] = []
|
||||
|
||||
@property
|
||||
def input_profiles(self) -> List[AudioProfile]:
|
||||
"""Return a list of available input profiles."""
|
||||
return self._input
|
||||
def cards(self) -> List[SoundCard]:
|
||||
"""Return a list of available sound cards and profiles."""
|
||||
return self._cards
|
||||
|
||||
@property
|
||||
def output_profiles(self) -> List[AudioProfile]:
|
||||
"""Return a list of available output profiles."""
|
||||
return self._output
|
||||
def inputs(self) -> List[AudioStream]:
|
||||
"""Return a list of available input streams."""
|
||||
return self._inputs
|
||||
|
||||
@property
|
||||
def outputs(self) -> List[AudioStream]:
|
||||
"""Return a list of available output streams."""
|
||||
return self._outputs
|
||||
|
||||
async def set_default(self, source: SourceType, name: str) -> None:
|
||||
"""Set a profile to default input/output."""
|
||||
"""Set a stream to default input/output."""
|
||||
try:
|
||||
with Pulse(PULSE_NAME) as pulse:
|
||||
if source == SourceType.OUTPUT:
|
||||
|
@ -73,7 +97,7 @@ class SoundControl(CoreSysAttributes):
|
|||
await self.update()
|
||||
|
||||
async def set_volume(self, source: SourceType, name: str, volume: float) -> None:
|
||||
"""Set a profile to volume input/output."""
|
||||
"""Set a stream to volume input/output."""
|
||||
try:
|
||||
with Pulse(PULSE_NAME) as pulse:
|
||||
if source == SourceType.OUTPUT:
|
||||
|
@ -94,6 +118,37 @@ class SoundControl(CoreSysAttributes):
|
|||
# Reload data
|
||||
await self.update()
|
||||
|
||||
async def ativate_profile(self, card_name: str, profile_name: str) -> None:
|
||||
"""Set a profile to volume input/output."""
|
||||
try:
|
||||
with Pulse(PULSE_NAME) as pulse:
|
||||
|
||||
# Get card
|
||||
select_card = None
|
||||
for card in pulse.card_list():
|
||||
if card.name != card_name:
|
||||
continue
|
||||
select_card = card
|
||||
break
|
||||
|
||||
if not select_card:
|
||||
raise PulseIndexError()
|
||||
|
||||
# set profile
|
||||
pulse.card_profile_set(select_card, profile_name)
|
||||
|
||||
except PulseIndexError:
|
||||
_LOGGER.error("Can't find %s profile %s", card_name, profile_name)
|
||||
raise PulseAudioError() from None
|
||||
except PulseError as err:
|
||||
_LOGGER.error(
|
||||
"Can't activate %s profile %s: %s", card_name, profile_name, err
|
||||
)
|
||||
raise PulseAudioError() from None
|
||||
|
||||
# Reload data
|
||||
await self.update()
|
||||
|
||||
async def update(self):
|
||||
"""Update properties over dbus."""
|
||||
_LOGGER.info("Update PulseAudio information")
|
||||
|
@ -102,10 +157,10 @@ class SoundControl(CoreSysAttributes):
|
|||
server = pulse.server_info()
|
||||
|
||||
# Update output
|
||||
self._output.clear()
|
||||
self._outputs.clear()
|
||||
for sink in pulse.sink_list():
|
||||
self._output.append(
|
||||
AudioProfile(
|
||||
self._outputs.append(
|
||||
AudioStream(
|
||||
sink.name,
|
||||
sink.description,
|
||||
sink.volume.value_flat,
|
||||
|
@ -114,19 +169,41 @@ class SoundControl(CoreSysAttributes):
|
|||
)
|
||||
|
||||
# Update input
|
||||
self._input.clear()
|
||||
self._inputs.clear()
|
||||
for source in pulse.source_list():
|
||||
# Filter monitor devices out because we did not use it now
|
||||
if source.name.endswith(".monitor"):
|
||||
continue
|
||||
self._input.append(
|
||||
AudioProfile(
|
||||
self._inputs.append(
|
||||
AudioStream(
|
||||
source.name,
|
||||
source.description,
|
||||
source.volume.value_flat,
|
||||
source.name == server.default_source_name,
|
||||
)
|
||||
)
|
||||
|
||||
# Update Sound Card
|
||||
self._cards.clear()
|
||||
for card in pulse.card_list():
|
||||
sound_profiles: List[SoundProfile] = []
|
||||
|
||||
# Generate profiles
|
||||
for profile in card.profile_list:
|
||||
if not profile.available:
|
||||
continue
|
||||
sound_profiles.append(
|
||||
SoundProfile(
|
||||
profile.name,
|
||||
profile.description,
|
||||
profile.name == card.profile_active.name,
|
||||
)
|
||||
)
|
||||
|
||||
self._cards.append(
|
||||
SoundCard(card.name, card.description, sound_profiles)
|
||||
)
|
||||
|
||||
except PulseOperationFailed as err:
|
||||
_LOGGER.error("Error while processing pulse update: %s", err)
|
||||
raise PulseAudioError() from None
|
||||
|
|
|
@ -12,7 +12,6 @@ import pyudev
|
|||
from ..const import ATTR_DEVICES, ATTR_NAME, ATTR_TYPE, CHAN_ID, CHAN_TYPE
|
||||
from ..exceptions import HardwareNotSupportedError
|
||||
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
ASOUND_CARDS: Path = Path("/proc/asound/cards")
|
||||
|
|
Loading…
Reference in New Issue