mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-25 18:16:32 +00:00
commit
14167f6e13
84
API.md
84
API.md
@ -866,6 +866,7 @@ return:
|
|||||||
"card": [
|
"card": [
|
||||||
{
|
{
|
||||||
"name": "...",
|
"name": "...",
|
||||||
|
"index": 1,
|
||||||
"driver": "...",
|
"driver": "...",
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
@ -879,17 +880,56 @@ return:
|
|||||||
"input": [
|
"input": [
|
||||||
{
|
{
|
||||||
"name": "...",
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
"description": "...",
|
"description": "...",
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"default": false
|
"mute": false,
|
||||||
|
"default": false,
|
||||||
|
"card": "null|int",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "INPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"output": [
|
"output": [
|
||||||
{
|
{
|
||||||
"name": "...",
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
"description": "...",
|
"description": "...",
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"default": false
|
"mute": false,
|
||||||
|
"default": false,
|
||||||
|
"card": "null|int",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "OUTPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"application": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "OUTPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -914,7 +954,7 @@ return:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "...",
|
"index": "...",
|
||||||
"volume": 0.5
|
"volume": 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -923,11 +963,47 @@ return:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "...",
|
"index": "...",
|
||||||
"volume": 0.5
|
"volume": 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- POST `/audio/volume/{output|input}/application`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"volume": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/input`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/{output|input}/application`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- POST `/audio/default/input`
|
- POST `/audio/default/input`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -63,6 +63,8 @@ RE_WEBUI = re.compile(
|
|||||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||||
|
|
||||||
|
|
||||||
class Addon(AddonModel):
|
class Addon(AddonModel):
|
||||||
"""Hold data for add-on inside Supervisor."""
|
"""Hold data for add-on inside Supervisor."""
|
||||||
@ -282,7 +284,13 @@ class Addon(AddonModel):
|
|||||||
"""Return a pulse profile for output or None."""
|
"""Return a pulse profile for output or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
|
||||||
|
# Fallback with old audio settings
|
||||||
|
# Remove after 210
|
||||||
|
output_data = self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||||
|
if output_data and RE_OLD_AUDIO.fullmatch(output_data):
|
||||||
|
return None
|
||||||
|
return output_data
|
||||||
|
|
||||||
@audio_output.setter
|
@audio_output.setter
|
||||||
def audio_output(self, value: Optional[str]):
|
def audio_output(self, value: Optional[str]):
|
||||||
@ -297,7 +305,13 @@ class Addon(AddonModel):
|
|||||||
"""Return pulse profile for input or None."""
|
"""Return pulse profile for input or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
|
||||||
|
# Fallback with old audio settings
|
||||||
|
# Remove after 210
|
||||||
|
input_data = self.persist.get(ATTR_AUDIO_INPUT)
|
||||||
|
if input_data and RE_OLD_AUDIO.fullmatch(input_data):
|
||||||
|
return None
|
||||||
|
return input_data
|
||||||
|
|
||||||
@audio_input.setter
|
@audio_input.setter
|
||||||
def audio_input(self, value: Optional[str]):
|
def audio_input(self, value: Optional[str]):
|
||||||
|
@ -31,6 +31,7 @@ from ..const import (
|
|||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
|
ATTR_INIT,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@ -344,6 +345,11 @@ class AddonModel(CoreSysAttributes):
|
|||||||
"""Return Exclude list for snapshot."""
|
"""Return Exclude list for snapshot."""
|
||||||
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_init(self) -> bool:
|
||||||
|
"""Return True if the add-on have no own init."""
|
||||||
|
return self.data[ATTR_INIT]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_stdin(self) -> bool:
|
def with_stdin(self) -> bool:
|
||||||
"""Return True if the add-on access use stdin input."""
|
"""Return True if the add-on access use stdin input."""
|
||||||
|
@ -44,6 +44,7 @@ from ..const import (
|
|||||||
ATTR_INGRESS_PANEL,
|
ATTR_INGRESS_PANEL,
|
||||||
ATTR_INGRESS_PORT,
|
ATTR_INGRESS_PORT,
|
||||||
ATTR_INGRESS_TOKEN,
|
ATTR_INGRESS_TOKEN,
|
||||||
|
ATTR_INIT,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@ -189,6 +190,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_URL): vol.Url(),
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
||||||
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
|
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
||||||
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
||||||
|
@ -330,7 +330,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post("/audio/restart", api_audio.restart),
|
web.post("/audio/restart", api_audio.restart),
|
||||||
web.post("/audio/reload", api_audio.reload),
|
web.post("/audio/reload", api_audio.reload),
|
||||||
web.post("/audio/profile", api_audio.set_profile),
|
web.post("/audio/profile", api_audio.set_profile),
|
||||||
|
web.post("/audio/volume/{source}/application", api_audio.set_volume),
|
||||||
web.post("/audio/volume/{source}", api_audio.set_volume),
|
web.post("/audio/volume/{source}", api_audio.set_volume),
|
||||||
|
web.post("/audio/mute/{source}/application", api_audio.set_mute),
|
||||||
|
web.post("/audio/mute/{source}", api_audio.set_mute),
|
||||||
web.post("/audio/default/{source}", api_audio.set_default),
|
web.post("/audio/default/{source}", api_audio.set_default),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -8,12 +8,15 @@ import attr
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
ATTR_ACTIVE,
|
||||||
|
ATTR_APPLICATION,
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_CARD,
|
ATTR_CARD,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_HOST,
|
ATTR_HOST,
|
||||||
|
ATTR_INDEX,
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
ATTR_LATEST_VERSION,
|
ATTR_LATEST_VERSION,
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
@ -38,11 +41,19 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
|||||||
|
|
||||||
SCHEMA_VOLUME = vol.Schema(
|
SCHEMA_VOLUME = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||||
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_MUTE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||||
|
vol.Required(ATTR_ACTIVE): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
||||||
|
|
||||||
SCHEMA_PROFILE = vol.Schema(
|
SCHEMA_PROFILE = vol.Schema(
|
||||||
@ -68,6 +79,9 @@ class APIAudio(CoreSysAttributes):
|
|||||||
ATTR_OUTPUT: [
|
ATTR_OUTPUT: [
|
||||||
attr.asdict(stream) for stream in self.sys_host.sound.outputs
|
attr.asdict(stream) for stream in self.sys_host.sound.outputs
|
||||||
],
|
],
|
||||||
|
ATTR_APPLICATION: [
|
||||||
|
attr.asdict(stream) for stream in self.sys_host.sound.applications
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,10 +130,26 @@ class APIAudio(CoreSysAttributes):
|
|||||||
async def set_volume(self, request: web.Request) -> None:
|
async def set_volume(self, request: web.Request) -> None:
|
||||||
"""Set audio volume on stream."""
|
"""Set audio volume on stream."""
|
||||||
source: StreamType = StreamType(request.match_info.get("source"))
|
source: StreamType = StreamType(request.match_info.get("source"))
|
||||||
|
application: bool = request.path.endswith("application")
|
||||||
body = await api_validate(SCHEMA_VOLUME, request)
|
body = await api_validate(SCHEMA_VOLUME, request)
|
||||||
|
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_host.sound.set_volume(source, body[ATTR_NAME], body[ATTR_VOLUME])
|
self.sys_host.sound.set_volume(
|
||||||
|
source, body[ATTR_INDEX], body[ATTR_VOLUME], application
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def set_mute(self, request: web.Request) -> None:
|
||||||
|
"""Mute audio volume on stream."""
|
||||||
|
source: StreamType = StreamType(request.match_info.get("source"))
|
||||||
|
application: bool = request.path.endswith("application")
|
||||||
|
body = await api_validate(SCHEMA_MUTE, request)
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_host.sound.set_mute(
|
||||||
|
source, body[ATTR_INDEX], body[ATTR_ACTIVE], application
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@ -3,12 +3,12 @@ import asyncio
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Awaitable, Optional
|
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Awaitable, List, Optional
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from .const import ATTR_VERSION, FILE_HASSIO_AUDIO
|
from .const import ATTR_VERSION, FILE_HASSIO_AUDIO, STATE_STARTED
|
||||||
from .coresys import CoreSys, CoreSysAttributes
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
from .docker.audio import DockerAudio
|
from .docker.audio import DockerAudio
|
||||||
from .docker.stats import DockerStats
|
from .docker.stats import DockerStats
|
||||||
@ -157,6 +157,8 @@ class Audio(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.error("Can't start Audio plugin")
|
_LOGGER.error("Can't start Audio plugin")
|
||||||
raise AudioError() from None
|
raise AudioError() from None
|
||||||
|
|
||||||
|
await self._restart_audio_addons()
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Run CoreDNS."""
|
"""Run CoreDNS."""
|
||||||
# Start Instance
|
# Start Instance
|
||||||
@ -167,6 +169,8 @@ class Audio(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.error("Can't start Audio plugin")
|
_LOGGER.error("Can't start Audio plugin")
|
||||||
raise AudioError() from None
|
raise AudioError() from None
|
||||||
|
|
||||||
|
await self._restart_audio_addons()
|
||||||
|
|
||||||
def logs(self) -> Awaitable[bytes]:
|
def logs(self) -> Awaitable[bytes]:
|
||||||
"""Get CoreDNS docker logs.
|
"""Get CoreDNS docker logs.
|
||||||
|
|
||||||
@ -217,3 +221,22 @@ class Audio(JsonConfig, CoreSysAttributes):
|
|||||||
default_source=input_profile,
|
default_source=input_profile,
|
||||||
default_sink=output_profile,
|
default_sink=output_profile,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _restart_audio_addons(self):
|
||||||
|
"""Restart all Add-ons they can be connect to unix socket."""
|
||||||
|
tasks: List[Awaitable[None]] = []
|
||||||
|
|
||||||
|
# Find all Add-ons running with audio
|
||||||
|
for addon in self.sys_addons.installed:
|
||||||
|
if not addon.with_audio:
|
||||||
|
continue
|
||||||
|
if await addon.state() != STATE_STARTED:
|
||||||
|
continue
|
||||||
|
tasks.append(addon.restart())
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
return
|
||||||
|
|
||||||
|
# restart
|
||||||
|
_LOGGER.info("Restart all Add-ons attach to pulse server: %d", len(tasks))
|
||||||
|
await asyncio.wait(tasks)
|
||||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SUPERVISOR_VERSION = "203"
|
SUPERVISOR_VERSION = "204"
|
||||||
|
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
@ -234,6 +234,10 @@ ATTR_CLI = "cli"
|
|||||||
ATTR_DEFAULT = "default"
|
ATTR_DEFAULT = "default"
|
||||||
ATTR_VOLUME = "volume"
|
ATTR_VOLUME = "volume"
|
||||||
ATTR_CARD = "card"
|
ATTR_CARD = "card"
|
||||||
|
ATTR_INDEX = "index"
|
||||||
|
ATTR_ACTIVE = "active"
|
||||||
|
ATTR_APPLICATION = "application"
|
||||||
|
ATTR_INIT = "init"
|
||||||
|
|
||||||
PROVIDE_SERVICE = "provide"
|
PROVIDE_SERVICE = "provide"
|
||||||
NEED_SERVICE = "need"
|
NEED_SERVICE = "need"
|
||||||
|
@ -344,7 +344,7 @@ class DockerAddon(DockerInterface):
|
|||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=self.addon.hostname,
|
hostname=self.addon.hostname,
|
||||||
detach=True,
|
detach=True,
|
||||||
init=True,
|
init=self.addon.default_init,
|
||||||
privileged=self.full_access,
|
privileged=self.full_access,
|
||||||
ipc_mode=self.ipc,
|
ipc_mode=self.ipc,
|
||||||
stdin_open=self.addon.with_stdin,
|
stdin_open=self.addon.with_stdin,
|
||||||
|
@ -41,6 +41,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
self.image,
|
self.image,
|
||||||
version=self.sys_audio.version,
|
version=self.sys_audio.version,
|
||||||
|
init=False,
|
||||||
ipv4=self.sys_docker.network.audio,
|
ipv4=self.sys_docker.network.audio,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=self.name.replace("_", "-"),
|
hostname=self.name.replace("_", "-"),
|
||||||
|
@ -41,6 +41,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
self.image,
|
self.image,
|
||||||
version=self.sys_dns.version,
|
version=self.sys_dns.version,
|
||||||
|
init=False,
|
||||||
dns=False,
|
dns=False,
|
||||||
ipv4=self.sys_docker.network.dns,
|
ipv4=self.sys_docker.network.dns,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed
|
from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed
|
||||||
@ -24,13 +24,30 @@ class StreamType(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True)
|
@attr.s(frozen=True)
|
||||||
class AudioStream:
|
class AudioApplication:
|
||||||
"""Represent a input/output profile."""
|
"""Represent a application on the stream."""
|
||||||
|
|
||||||
name: str = attr.ib()
|
name: str = attr.ib()
|
||||||
|
index: int = attr.ib()
|
||||||
|
stream_index: str = attr.ib()
|
||||||
|
stream_type: StreamType = attr.ib()
|
||||||
|
volume: float = attr.ib()
|
||||||
|
mute: bool = attr.ib()
|
||||||
|
addon: str = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(frozen=True)
|
||||||
|
class AudioStream:
|
||||||
|
"""Represent a input/output stream."""
|
||||||
|
|
||||||
|
name: str = attr.ib()
|
||||||
|
index: int = attr.ib()
|
||||||
description: str = attr.ib()
|
description: str = attr.ib()
|
||||||
volume: float = attr.ib()
|
volume: float = attr.ib()
|
||||||
|
mute: bool = attr.ib()
|
||||||
default: bool = attr.ib()
|
default: bool = attr.ib()
|
||||||
|
card: Optional[int] = attr.ib()
|
||||||
|
applications: List[AudioApplication] = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True)
|
@attr.s(frozen=True)
|
||||||
@ -47,6 +64,7 @@ class SoundCard:
|
|||||||
"""Represent a Sound Card."""
|
"""Represent a Sound Card."""
|
||||||
|
|
||||||
name: str = attr.ib()
|
name: str = attr.ib()
|
||||||
|
index: int = attr.ib()
|
||||||
driver: str = attr.ib()
|
driver: str = attr.ib()
|
||||||
profiles: List[SoundProfile] = attr.ib()
|
profiles: List[SoundProfile] = attr.ib()
|
||||||
|
|
||||||
@ -60,6 +78,7 @@ class SoundControl(CoreSysAttributes):
|
|||||||
self._cards: List[SoundCard] = []
|
self._cards: List[SoundCard] = []
|
||||||
self._inputs: List[AudioStream] = []
|
self._inputs: List[AudioStream] = []
|
||||||
self._outputs: List[AudioStream] = []
|
self._outputs: List[AudioStream] = []
|
||||||
|
self._applications: List[AudioApplication] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cards(self) -> List[SoundCard]:
|
def cards(self) -> List[SoundCard]:
|
||||||
@ -76,6 +95,11 @@ class SoundControl(CoreSysAttributes):
|
|||||||
"""Return a list of available output streams."""
|
"""Return a list of available output streams."""
|
||||||
return self._outputs
|
return self._outputs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def applications(self) -> List[AudioApplication]:
|
||||||
|
"""Return a list of available application streams."""
|
||||||
|
return self._applications
|
||||||
|
|
||||||
async def set_default(self, stream_type: StreamType, name: str) -> None:
|
async def set_default(self, stream_type: StreamType, name: str) -> None:
|
||||||
"""Set a stream to default input/output."""
|
"""Set a stream to default input/output."""
|
||||||
|
|
||||||
@ -90,11 +114,12 @@ class SoundControl(CoreSysAttributes):
|
|||||||
# Get sink and set it as default
|
# Get sink and set it as default
|
||||||
sink = pulse.get_sink_by_name(name)
|
sink = pulse.get_sink_by_name(name)
|
||||||
pulse.sink_default_set(sink)
|
pulse.sink_default_set(sink)
|
||||||
|
|
||||||
except PulseIndexError:
|
except PulseIndexError:
|
||||||
_LOGGER.error("Can't find %s profile %s", source, name)
|
_LOGGER.error("Can't find %s stream %s", source, name)
|
||||||
raise PulseAudioError() from None
|
raise PulseAudioError() from None
|
||||||
except PulseError as err:
|
except PulseError as err:
|
||||||
_LOGGER.error("Can't set %s as default: %s", name, err)
|
_LOGGER.error("Can't set %s as stream: %s", name, err)
|
||||||
raise PulseAudioError() from None
|
raise PulseAudioError() from None
|
||||||
|
|
||||||
# Run and Reload data
|
# Run and Reload data
|
||||||
@ -102,32 +127,73 @@ class SoundControl(CoreSysAttributes):
|
|||||||
await self.update()
|
await self.update()
|
||||||
|
|
||||||
async def set_volume(
|
async def set_volume(
|
||||||
self, stream_type: StreamType, name: str, volume: float
|
self, stream_type: StreamType, index: int, volume: float, application: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set a stream to volume input/output."""
|
"""Set a stream to volume input/output/application."""
|
||||||
|
|
||||||
def _set_volume():
|
def _set_volume():
|
||||||
try:
|
try:
|
||||||
with Pulse(PULSE_NAME) as pulse:
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
if stream_type == StreamType.INPUT:
|
if stream_type == StreamType.INPUT:
|
||||||
# Get source and set it as default
|
if application:
|
||||||
stream = pulse.get_source_by_name(name)
|
stream = pulse.source_output_info(index)
|
||||||
|
else:
|
||||||
|
stream = pulse.source_info(index)
|
||||||
else:
|
else:
|
||||||
# Get sink and set it as default
|
if application:
|
||||||
stream = pulse.get_sink_by_name(name)
|
stream = pulse.sink_input_info(index)
|
||||||
|
else:
|
||||||
|
stream = pulse.sink_info(index)
|
||||||
|
|
||||||
|
# Set volume
|
||||||
pulse.volume_set_all_chans(stream, volume)
|
pulse.volume_set_all_chans(stream, volume)
|
||||||
except PulseIndexError:
|
except PulseIndexError:
|
||||||
_LOGGER.error("Can't find %s profile %s", stream_type, name)
|
_LOGGER.error(
|
||||||
|
"Can't find %s stream %d (App: %s)", stream_type, index, application
|
||||||
|
)
|
||||||
raise PulseAudioError() from None
|
raise PulseAudioError() from None
|
||||||
except PulseError as err:
|
except PulseError as err:
|
||||||
_LOGGER.error("Can't set %s volume: %s", name, err)
|
_LOGGER.error("Can't set %d volume: %s", index, err)
|
||||||
raise PulseAudioError() from None
|
raise PulseAudioError() from None
|
||||||
|
|
||||||
# Run and Reload data
|
# Run and Reload data
|
||||||
await self.sys_run_in_executor(_set_volume)
|
await self.sys_run_in_executor(_set_volume)
|
||||||
await self.update()
|
await self.update()
|
||||||
|
|
||||||
|
async def set_mute(
|
||||||
|
self, stream_type: StreamType, index: int, mute: bool, application: bool
|
||||||
|
) -> None:
|
||||||
|
"""Set a stream to mute input/output/application."""
|
||||||
|
|
||||||
|
def _set_mute():
|
||||||
|
try:
|
||||||
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
|
if stream_type == StreamType.INPUT:
|
||||||
|
if application:
|
||||||
|
stream = pulse.source_output_info(index)
|
||||||
|
else:
|
||||||
|
stream = pulse.source_info(index)
|
||||||
|
else:
|
||||||
|
if application:
|
||||||
|
stream = pulse.sink_input_info(index)
|
||||||
|
else:
|
||||||
|
stream = pulse.sink_info(index)
|
||||||
|
|
||||||
|
# Mute stream
|
||||||
|
pulse.mute(stream, mute)
|
||||||
|
except PulseIndexError:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Can't find %s stream %d (App: %s)", stream_type, index, application
|
||||||
|
)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
except PulseError as err:
|
||||||
|
_LOGGER.error("Can't set %d volume: %s", index, err)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
|
||||||
|
# Run and Reload data
|
||||||
|
await self.sys_run_in_executor(_set_mute)
|
||||||
|
await self.update()
|
||||||
|
|
||||||
async def ativate_profile(self, card_name: str, profile_name: str) -> None:
|
async def ativate_profile(self, card_name: str, profile_name: str) -> None:
|
||||||
"""Set a profile to volume input/output."""
|
"""Set a profile to volume input/output."""
|
||||||
|
|
||||||
@ -160,15 +226,59 @@ class SoundControl(CoreSysAttributes):
|
|||||||
with Pulse(PULSE_NAME) as pulse:
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
server = pulse.server_info()
|
server = pulse.server_info()
|
||||||
|
|
||||||
|
# Update applications
|
||||||
|
self._applications.clear()
|
||||||
|
for application in pulse.sink_input_list():
|
||||||
|
self._applications.append(
|
||||||
|
AudioApplication(
|
||||||
|
application.proplist.get(
|
||||||
|
"application.name", application.name
|
||||||
|
),
|
||||||
|
application.index,
|
||||||
|
application.sink,
|
||||||
|
StreamType.OUTPUT,
|
||||||
|
application.volume.value_flat,
|
||||||
|
bool(application.mute),
|
||||||
|
application.proplist.get(
|
||||||
|
"application.process.machine_id", ""
|
||||||
|
).replace("-", "_"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for application in pulse.source_output_list():
|
||||||
|
self._applications.append(
|
||||||
|
AudioApplication(
|
||||||
|
application.proplist.get(
|
||||||
|
"application.name", application.name
|
||||||
|
),
|
||||||
|
application.index,
|
||||||
|
application.source,
|
||||||
|
StreamType.INPUT,
|
||||||
|
application.volume.value_flat,
|
||||||
|
bool(application.mute),
|
||||||
|
application.proplist.get(
|
||||||
|
"application.process.machine_id", ""
|
||||||
|
).replace("-", "_"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Update output
|
# Update output
|
||||||
self._outputs.clear()
|
self._outputs.clear()
|
||||||
for sink in pulse.sink_list():
|
for sink in pulse.sink_list():
|
||||||
self._outputs.append(
|
self._outputs.append(
|
||||||
AudioStream(
|
AudioStream(
|
||||||
sink.name,
|
sink.name,
|
||||||
|
sink.index,
|
||||||
sink.description,
|
sink.description,
|
||||||
sink.volume.value_flat,
|
sink.volume.value_flat,
|
||||||
|
bool(sink.mute),
|
||||||
sink.name == server.default_sink_name,
|
sink.name == server.default_sink_name,
|
||||||
|
sink.card if sink.card != 0xFFFFFFFF else None,
|
||||||
|
[
|
||||||
|
application
|
||||||
|
for application in self._applications
|
||||||
|
if application.stream_index == sink.index
|
||||||
|
and application.stream_type == StreamType.OUTPUT
|
||||||
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -181,9 +291,18 @@ class SoundControl(CoreSysAttributes):
|
|||||||
self._inputs.append(
|
self._inputs.append(
|
||||||
AudioStream(
|
AudioStream(
|
||||||
source.name,
|
source.name,
|
||||||
|
source.index,
|
||||||
source.description,
|
source.description,
|
||||||
source.volume.value_flat,
|
source.volume.value_flat,
|
||||||
|
bool(source.mute),
|
||||||
source.name == server.default_source_name,
|
source.name == server.default_source_name,
|
||||||
|
source.card if source.card != 0xFFFFFFFF else None,
|
||||||
|
[
|
||||||
|
application
|
||||||
|
for application in self._applications
|
||||||
|
if application.stream_index == source.index
|
||||||
|
and application.stream_type == StreamType.INPUT
|
||||||
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -205,7 +324,9 @@ class SoundControl(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._cards.append(
|
self._cards.append(
|
||||||
SoundCard(card.name, card.driver, sound_profiles)
|
SoundCard(
|
||||||
|
card.name, card.index, card.driver, sound_profiles
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
except PulseOperationFailed as err:
|
except PulseOperationFailed as err:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user