mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-09 02:06:30 +00:00
Add Pulse audio control basics (#1525)
* Add Pulse audio control basics * add functionality * Fix handling * Give access to all * Fix latest issues * revert docker * Fix pipeline
This commit is contained in:
parent
ae8ddca040
commit
2495cda5ec
@ -38,6 +38,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
jq \
|
jq \
|
||||||
dbus \
|
dbus \
|
||||||
network-manager \
|
network-manager \
|
||||||
|
libpulse0 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python dependencies from requirements.txt if it exists
|
# Install Python dependencies from requirements.txt if it exists
|
||||||
|
56
API.md
56
API.md
@ -861,7 +861,25 @@ return:
|
|||||||
{
|
{
|
||||||
"host": "ip-address",
|
"host": "ip-address",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"latest_version": "2"
|
"latest_version": "2",
|
||||||
|
"audio": {
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"description": "...",
|
||||||
|
"volume": 0.3,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"description": "...",
|
||||||
|
"volume": 0.3,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -875,8 +893,44 @@ return:
|
|||||||
|
|
||||||
- POST `/audio/restart`
|
- POST `/audio/restart`
|
||||||
|
|
||||||
|
- POST `/audio/reload`
|
||||||
|
|
||||||
- GET `/audio/logs`
|
- GET `/audio/logs`
|
||||||
|
|
||||||
|
- POST `/audio/volume/input`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"volume": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/volume/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"volume": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/default/input`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/default/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- GET `/audio/stats`
|
- GET `/audio/stats`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
17
Dockerfile
17
Dockerfile
@ -3,14 +3,15 @@ FROM $BUILD_FROM
|
|||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
openssl \
|
|
||||||
libffi \
|
|
||||||
musl \
|
|
||||||
git \
|
|
||||||
socat \
|
|
||||||
glib \
|
|
||||||
eudev \
|
eudev \
|
||||||
eudev-libs
|
eudev-libs \
|
||||||
|
git \
|
||||||
|
glib \
|
||||||
|
libffi \
|
||||||
|
libpulse \
|
||||||
|
musl \
|
||||||
|
openssl \
|
||||||
|
socat
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
ARG BUILD_ARCH
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
@ -18,7 +19,7 @@ WORKDIR /usr/src
|
|||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN export MAKEFLAGS="-j$(nproc)" \
|
RUN export MAKEFLAGS="-j$(nproc)" \
|
||||||
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
|
&& pip3 install --no-cache-dir --no-index --only-binary=:all: \
|
||||||
"https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
|
"https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
|
||||||
-r ./requirements.txt \
|
-r ./requirements.txt \
|
||||||
&& rm -f requirements.txt
|
&& rm -f requirements.txt
|
||||||
|
@ -17,6 +17,10 @@ jobs:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: "ubuntu-latest"
|
vmImage: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
|
- script: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libpulse0 libudev1
|
||||||
|
displayName: "Install Host library"
|
||||||
- task: UsePythonVersion@0
|
- task: UsePythonVersion@0
|
||||||
displayName: "Use Python 3.7"
|
displayName: "Use Python 3.7"
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -8,9 +8,10 @@ cryptography==2.8
|
|||||||
docker==4.2.0
|
docker==4.2.0
|
||||||
gitpython==3.1.0
|
gitpython==3.1.0
|
||||||
packaging==20.1
|
packaging==20.1
|
||||||
|
ptvsd==4.3.2
|
||||||
|
pulsectl==20.2.2
|
||||||
pytz==2019.3
|
pytz==2019.3
|
||||||
pyudev==0.22.0
|
pyudev==0.22.0
|
||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
uvloop==0.14.0
|
uvloop==0.14.0
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
ptvsd==4.3.2
|
|
||||||
|
35
rootfs/etc/pulse/client.conf
Normal file
35
rootfs/etc/pulse/client.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# This file is part of PulseAudio.
|
||||||
|
#
|
||||||
|
# PulseAudio is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PulseAudio is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
## Configuration file for PulseAudio clients. See pulse-client.conf(5) for
|
||||||
|
## more information. Default values are commented out. Use either ; or # for
|
||||||
|
## commenting.
|
||||||
|
|
||||||
|
; default-sink =
|
||||||
|
; default-source =
|
||||||
|
default-server = unix://data/audio/external/pulse.sock
|
||||||
|
; default-dbus-server =
|
||||||
|
|
||||||
|
autospawn = no
|
||||||
|
; daemon-binary = /usr/bin/pulseaudio
|
||||||
|
; extra-arguments = --log-target=syslog
|
||||||
|
|
||||||
|
; cookie-file =
|
||||||
|
|
||||||
|
; enable-shm = yes
|
||||||
|
; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB
|
||||||
|
|
||||||
|
; auto-connect-localhost = no
|
||||||
|
; auto-connect-display = no
|
@ -62,6 +62,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_info()
|
self._register_info()
|
||||||
self._register_auth()
|
self._register_auth()
|
||||||
self._register_dns()
|
self._register_dns()
|
||||||
|
self._register_audio()
|
||||||
|
|
||||||
def _register_host(self) -> None:
|
def _register_host(self) -> None:
|
||||||
"""Register hostcontrol functions."""
|
"""Register hostcontrol functions."""
|
||||||
@ -327,6 +328,9 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/audio/logs", api_audio.logs),
|
web.get("/audio/logs", api_audio.logs),
|
||||||
web.post("/audio/update", api_audio.update),
|
web.post("/audio/update", api_audio.update),
|
||||||
web.post("/audio/restart", api_audio.restart),
|
web.post("/audio/restart", api_audio.restart),
|
||||||
|
web.post("/audio/reload", api_audio.reload),
|
||||||
|
web.post("/audio/volume/{source}", api_audio.set_volume),
|
||||||
|
web.post("/audio/default/{source}", api_audio.set_default),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,30 +4,46 @@ import logging
|
|||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
import attr
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
ATTR_AUDIO,
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_HOST,
|
ATTR_HOST,
|
||||||
|
ATTR_INPUT,
|
||||||
ATTR_LATEST_VERSION,
|
ATTR_LATEST_VERSION,
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_NAME,
|
||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_OUTPUT,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VOLUME,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
|
from ..host.sound import SourceType
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||||
|
|
||||||
|
SCHEMA_VOLUME = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||||
|
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
||||||
|
|
||||||
|
|
||||||
class APIAudio(CoreSysAttributes):
|
class APIAudio(CoreSysAttributes):
|
||||||
"""Handle RESTful API for Audio functions."""
|
"""Handle RESTful API for Audio functions."""
|
||||||
@ -39,6 +55,16 @@ class APIAudio(CoreSysAttributes):
|
|||||||
ATTR_VERSION: self.sys_audio.version,
|
ATTR_VERSION: self.sys_audio.version,
|
||||||
ATTR_LATEST_VERSION: self.sys_audio.latest_version,
|
ATTR_LATEST_VERSION: self.sys_audio.latest_version,
|
||||||
ATTR_HOST: str(self.sys_docker.network.audio),
|
ATTR_HOST: str(self.sys_docker.network.audio),
|
||||||
|
ATTR_AUDIO: {
|
||||||
|
ATTR_INPUT: [
|
||||||
|
attr.asdict(profile)
|
||||||
|
for profile in self.sys_host.sound.input_profiles
|
||||||
|
],
|
||||||
|
ATTR_OUTPUT: [
|
||||||
|
attr.asdict(profile)
|
||||||
|
for profile in self.sys_host.sound.output_profiles
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@ -76,3 +102,26 @@ class APIAudio(CoreSysAttributes):
|
|||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Audio plugin."""
|
"""Restart Audio plugin."""
|
||||||
return asyncio.shield(self.sys_audio.restart())
|
return asyncio.shield(self.sys_audio.restart())
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||||
|
"""Reload Audio information."""
|
||||||
|
return asyncio.shield(self.sys_host.sound.update())
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def set_volume(self, request: web.Request) -> None:
|
||||||
|
"""Set Audio information."""
|
||||||
|
source: SourceType = SourceType(request.match_info.get("source"))
|
||||||
|
body = await api_validate(SCHEMA_VOLUME, request)
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_host.sound.set_volume(source, body[ATTR_NAME], body[ATTR_VOLUME])
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def set_default(self, request: web.Request) -> None:
|
||||||
|
"""Set Audio default sources."""
|
||||||
|
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]))
|
||||||
|
@ -38,7 +38,18 @@ class APIHardware(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def audio(self, request: web.Request) -> Dict[str, Any]:
|
async def audio(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Show pulse audio profiles."""
|
"""Show pulse audio profiles."""
|
||||||
return {ATTR_AUDIO: {ATTR_INPUT: [], ATTR_OUTPUT: []}}
|
return {
|
||||||
|
ATTR_AUDIO: {
|
||||||
|
ATTR_INPUT: {
|
||||||
|
profile.name: profile.description
|
||||||
|
for profile in self.sys_host.sound.input_profiles
|
||||||
|
},
|
||||||
|
ATTR_OUTPUT: {
|
||||||
|
profile.name: profile.description
|
||||||
|
for profile in self.sys_host.sound.output_profiles
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def trigger(self, request: web.Request) -> None:
|
def trigger(self, request: web.Request) -> None:
|
||||||
|
@ -73,6 +73,7 @@ ADDONS_ROLE_ACCESS = {
|
|||||||
),
|
),
|
||||||
ROLE_MANAGER: re.compile(
|
ROLE_MANAGER: re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
|
r"|/audio/.*"
|
||||||
r"|/dns/.*"
|
r"|/dns/.*"
|
||||||
r"|/core/.+"
|
r"|/core/.+"
|
||||||
r"|/homeassistant/.+"
|
r"|/homeassistant/.+"
|
||||||
|
@ -231,6 +231,8 @@ ATTR_DOCUMENTATION = "documentation"
|
|||||||
ATTR_ADVANCED = "advanced"
|
ATTR_ADVANCED = "advanced"
|
||||||
ATTR_STAGE = "stage"
|
ATTR_STAGE = "stage"
|
||||||
ATTR_CLI = "cli"
|
ATTR_CLI = "cli"
|
||||||
|
ATTR_DEFAULT = "default"
|
||||||
|
ATTR_VOLUME = "volume"
|
||||||
|
|
||||||
PROVIDE_SERVICE = "provide"
|
PROVIDE_SERVICE = "provide"
|
||||||
NEED_SERVICE = "need"
|
NEED_SERVICE = "need"
|
||||||
|
@ -210,3 +210,10 @@ class DockerAPIError(HassioError):
|
|||||||
|
|
||||||
class HardwareNotSupportedError(HassioNotSupportedError):
|
class HardwareNotSupportedError(HassioNotSupportedError):
|
||||||
"""Raise if hardware function is not supported."""
|
"""Raise if hardware function is not supported."""
|
||||||
|
|
||||||
|
|
||||||
|
# Pulse Audio
|
||||||
|
|
||||||
|
|
||||||
|
class PulseAudioError(HassioError):
|
||||||
|
"""Raise if an sound error is happening."""
|
||||||
|
@ -2,20 +2,21 @@
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
FEATURES_HASSOS,
|
||||||
|
FEATURES_HOSTNAME,
|
||||||
|
FEATURES_REBOOT,
|
||||||
|
FEATURES_SERVICES,
|
||||||
|
FEATURES_SHUTDOWN,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import HassioError, PulseAudioError
|
||||||
from .apparmor import AppArmorControl
|
from .apparmor import AppArmorControl
|
||||||
from .control import SystemControl
|
from .control import SystemControl
|
||||||
from .info import InfoCenter
|
from .info import InfoCenter
|
||||||
from .services import ServiceManager
|
|
||||||
from .network import NetworkManager
|
from .network import NetworkManager
|
||||||
from ..const import (
|
from .services import ServiceManager
|
||||||
FEATURES_REBOOT,
|
from .sound import SoundControl
|
||||||
FEATURES_SHUTDOWN,
|
|
||||||
FEATURES_HOSTNAME,
|
|
||||||
FEATURES_SERVICES,
|
|
||||||
FEATURES_HASSOS,
|
|
||||||
)
|
|
||||||
from ..coresys import CoreSysAttributes, CoreSys
|
|
||||||
from ..exceptions import HassioError
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class HostManager(CoreSysAttributes):
|
|||||||
self._info: InfoCenter = InfoCenter(coresys)
|
self._info: InfoCenter = InfoCenter(coresys)
|
||||||
self._services: ServiceManager = ServiceManager(coresys)
|
self._services: ServiceManager = ServiceManager(coresys)
|
||||||
self._network: NetworkManager = NetworkManager(coresys)
|
self._network: NetworkManager = NetworkManager(coresys)
|
||||||
|
self._sound: SoundControl = SoundControl(coresys)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def apparmor(self) -> AppArmorControl:
|
def apparmor(self) -> AppArmorControl:
|
||||||
@ -58,6 +60,11 @@ class HostManager(CoreSysAttributes):
|
|||||||
"""Return host NetworkManager handler."""
|
"""Return host NetworkManager handler."""
|
||||||
return self._network
|
return self._network
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sound(self) -> SoundControl:
|
||||||
|
"""Return host PulseAudio control."""
|
||||||
|
return self._sound
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supperted_features(self):
|
def supperted_features(self):
|
||||||
"""Return a list of supported host features."""
|
"""Return a list of supported host features."""
|
||||||
@ -85,6 +92,9 @@ class HostManager(CoreSysAttributes):
|
|||||||
if self.sys_dbus.nmi_dns.is_connected:
|
if self.sys_dbus.nmi_dns.is_connected:
|
||||||
await self.network.update()
|
await self.network.update()
|
||||||
|
|
||||||
|
with suppress(PulseAudioError):
|
||||||
|
await self.sound.update()
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Load host information."""
|
"""Load host information."""
|
||||||
with suppress(HassioError):
|
with suppress(HassioError):
|
||||||
|
131
supervisor/host/sound.py
Normal file
131
supervisor/host/sound.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""Pulse host control."""
|
||||||
|
from enum import Enum
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed
|
||||||
|
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import PulseAudioError
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PULSE_NAME = "supervisor"
|
||||||
|
|
||||||
|
|
||||||
|
class SourceType(str, Enum):
|
||||||
|
"""INPUT/OUTPUT type of source."""
|
||||||
|
|
||||||
|
INPUT = "input"
|
||||||
|
OUTPUT = "output"
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(frozen=True)
|
||||||
|
class AudioProfile:
|
||||||
|
"""Represent a input/output profile."""
|
||||||
|
|
||||||
|
name: str = attr.ib()
|
||||||
|
description: str = attr.ib()
|
||||||
|
volume: float = attr.ib()
|
||||||
|
default: bool = 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] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_profiles(self) -> List[AudioProfile]:
|
||||||
|
"""Return a list of available input profiles."""
|
||||||
|
return self._input
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_profiles(self) -> List[AudioProfile]:
|
||||||
|
"""Return a list of available output profiles."""
|
||||||
|
return self._output
|
||||||
|
|
||||||
|
async def set_default(self, source: SourceType, name: str) -> None:
|
||||||
|
"""Set a profile to default input/output."""
|
||||||
|
try:
|
||||||
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
|
if source == SourceType.OUTPUT:
|
||||||
|
# Get source and set it as default
|
||||||
|
source = pulse.get_source_by_name(name)
|
||||||
|
pulse.source_default_set(source)
|
||||||
|
else:
|
||||||
|
# Get sink and set it as default
|
||||||
|
sink = pulse.get_sink_by_name(name)
|
||||||
|
pulse.sink_default_set(sink)
|
||||||
|
except PulseIndexError:
|
||||||
|
_LOGGER.error("Can't find %s profile %s", source, name)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
except PulseError as err:
|
||||||
|
_LOGGER.error("Can't set %s as default: %s", name, err)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
|
||||||
|
# Reload data
|
||||||
|
await self.update()
|
||||||
|
|
||||||
|
async def set_volume(self, source: SourceType, name: str, volume: float) -> None:
|
||||||
|
"""Set a profile to volume input/output."""
|
||||||
|
try:
|
||||||
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
|
if source == SourceType.OUTPUT:
|
||||||
|
# Get source and set it as default
|
||||||
|
source = pulse.get_source_by_name(name)
|
||||||
|
else:
|
||||||
|
# Get sink and set it as default
|
||||||
|
source = pulse.get_sink_by_name(name)
|
||||||
|
|
||||||
|
pulse.volume_set_all_chans(source, volume)
|
||||||
|
except PulseIndexError:
|
||||||
|
_LOGGER.error("Can't find %s profile %s", source, name)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
except PulseError as err:
|
||||||
|
_LOGGER.error("Can't set %s volume: %s", name, err)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
|
||||||
|
# Reload data
|
||||||
|
await self.update()
|
||||||
|
|
||||||
|
async def update(self):
|
||||||
|
"""Update properties over dbus."""
|
||||||
|
_LOGGER.info("Update PulseAudio information")
|
||||||
|
try:
|
||||||
|
with Pulse(PULSE_NAME) as pulse:
|
||||||
|
server = pulse.server_info()
|
||||||
|
|
||||||
|
# Update output
|
||||||
|
self._output.clear()
|
||||||
|
for sink in pulse.sink_list():
|
||||||
|
self._output.append(
|
||||||
|
AudioProfile(
|
||||||
|
sink.name,
|
||||||
|
sink.description,
|
||||||
|
sink.volume.value_flat,
|
||||||
|
sink.name == server.default_sink_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update input
|
||||||
|
self._input.clear()
|
||||||
|
for source in pulse.source_list():
|
||||||
|
self._input.append(
|
||||||
|
AudioProfile(
|
||||||
|
source.name,
|
||||||
|
source.description,
|
||||||
|
source.volume.value_flat,
|
||||||
|
source.name == server.default_source_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except PulseOperationFailed as err:
|
||||||
|
_LOGGER.error("Error while processing pulse update: %s", err)
|
||||||
|
raise PulseAudioError() from None
|
||||||
|
except PulseError as err:
|
||||||
|
_LOGGER.debug("Can't update PulseAudio data: %s", err)
|
@ -95,7 +95,6 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
"""
|
"""
|
||||||
url = URL_HASSIO_VERSION.format(channel=self.channel)
|
url = URL_HASSIO_VERSION.format(channel=self.channel)
|
||||||
machine = self.sys_machine or "default"
|
machine = self.sys_machine or "default"
|
||||||
board = self.sys_hassos.board
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Fetch update data from %s", url)
|
_LOGGER.info("Fetch update data from %s", url)
|
||||||
@ -123,8 +122,8 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine]
|
self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine]
|
||||||
|
|
||||||
# Update HassOS version
|
# Update HassOS version
|
||||||
if self.sys_hassos.available and board:
|
if self.sys_hassos.board:
|
||||||
self._data[ATTR_HASSOS] = data["hassos"][board]
|
self._data[ATTR_HASSOS] = data["hassos"][self.sys_hassos.board]
|
||||||
|
|
||||||
# Update Home Assistant services
|
# Update Home Assistant services
|
||||||
self._data[ATTR_CLI] = data["cli"]
|
self._data[ATTR_CLI] = data["cli"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user