mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 10:46:29 +00:00
Extend Audio support
This commit is contained in:
parent
7d02bb2fe9
commit
a2789ac540
21
API.md
21
API.md
@ -255,7 +255,11 @@ Optional:
|
||||
}
|
||||
```
|
||||
|
||||
- GET `/host/hardware`
|
||||
- POST `/host/reload`
|
||||
|
||||
### Hardware
|
||||
|
||||
- GET `/hardware/info`
|
||||
```json
|
||||
{
|
||||
"serial": ["/dev/xy"],
|
||||
@ -274,7 +278,20 @@ Optional:
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/host/reload`
|
||||
- GET `/hardware/audio`
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"input": {
|
||||
"0,0": "Mic"
|
||||
},
|
||||
"output": {
|
||||
"1,0": "Jack",
|
||||
"1,1": "HDMI"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Network
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Init file for HassIO addons."""
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
import json
|
||||
@ -372,15 +373,14 @@ class Addon(CoreSysAttributes):
|
||||
if not self.with_audio:
|
||||
return None
|
||||
|
||||
setting = self._config.audio_output
|
||||
if self.is_installed and \
|
||||
ATTR_AUDIO_OUTPUT in self._data.user[self._id]:
|
||||
setting = self._data.user[self._id][ATTR_AUDIO_OUTPUT]
|
||||
return setting
|
||||
return self._data.user[self._id][ATTR_AUDIO_OUTPUT]
|
||||
return self._audio.default.output
|
||||
|
||||
@audio_output.setter
|
||||
def audio_output(self, value):
|
||||
"""Set/remove custom audio output settings."""
|
||||
"""Set/reset audio output settings."""
|
||||
if value is None:
|
||||
self._data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None)
|
||||
else:
|
||||
@ -392,14 +392,13 @@ class Addon(CoreSysAttributes):
|
||||
if not self.with_audio:
|
||||
return None
|
||||
|
||||
setting = self._config.audio_input
|
||||
if self.is_installed and ATTR_AUDIO_INPUT in self._data.user[self._id]:
|
||||
setting = self._data.user[self._id][ATTR_AUDIO_INPUT]
|
||||
return setting
|
||||
return self._data.user[self._id][ATTR_AUDIO_INPUT]
|
||||
return self._audio.default.input
|
||||
|
||||
@audio_input.setter
|
||||
def audio_input(self, value):
|
||||
"""Set/remove custom audio input settings."""
|
||||
"""Set/reset audio input settings."""
|
||||
if value is None:
|
||||
self._data.user[self._id].pop(ATTR_AUDIO_INPUT, None)
|
||||
else:
|
||||
@ -504,6 +503,16 @@ class Addon(CoreSysAttributes):
|
||||
"""Return path to custom AppArmor profile."""
|
||||
return Path(self.path_location, 'apparmor')
|
||||
|
||||
@property
|
||||
def path_asound(self):
|
||||
"""Return path to asound config."""
|
||||
return Path(self._config.path_tmp, f"{self.slug}_asound")
|
||||
|
||||
@property
|
||||
def path_extern_asound(self):
|
||||
"""Return path to asound config for docker."""
|
||||
return Path(self._config.path_extern_tmp, f"{self.slug}_asound")
|
||||
|
||||
def save_data(self):
|
||||
"""Save data of addon."""
|
||||
self._addons.data.save_data()
|
||||
@ -526,6 +535,20 @@ class Addon(CoreSysAttributes):
|
||||
|
||||
return False
|
||||
|
||||
def write_asound(self):
|
||||
"""Write asound config to file and return True on success."""
|
||||
asound_config = self._audio.asound(
|
||||
alsa_input=self.audio_input, alsa_output=self.audio_output)
|
||||
|
||||
try:
|
||||
with self.path_asound.open('w') as config_file:
|
||||
config_file.write(asound_config)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Addon %s can't write asound: %s", self._id, err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
"""Create a schema for addon options."""
|
||||
@ -613,18 +636,24 @@ class Addon(CoreSysAttributes):
|
||||
@check_installed
|
||||
async def start(self):
|
||||
"""Set options and start addon."""
|
||||
# Options
|
||||
if not self.write_options():
|
||||
return False
|
||||
|
||||
# Sound
|
||||
if self.with_audio and not self.write_asound():
|
||||
return False
|
||||
|
||||
return await self.instance.run()
|
||||
|
||||
@check_installed
|
||||
def stop(self):
|
||||
"""Stop addon.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.stop()
|
||||
async def stop(self):
|
||||
"""Stop addon."""
|
||||
try:
|
||||
return self.instance.stop()
|
||||
finally:
|
||||
with suppress(OSError):
|
||||
self.path_asound.unlink()
|
||||
|
||||
@check_installed
|
||||
async def update(self):
|
||||
|
@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
from .audio import AlsaAudio
|
||||
from .addons import AddonManager
|
||||
from .api import RestAPI
|
||||
from .const import SOCKET_DOCKER
|
||||
@ -28,6 +29,7 @@ def initialize_coresys(loop):
|
||||
# Initialize core objects
|
||||
coresys.updater = Updater(coresys)
|
||||
coresys.api = RestAPI(coresys)
|
||||
coresys.audio = AlsaAudio(coresys)
|
||||
coresys.supervisor = Supervisor(coresys)
|
||||
coresys.homeassistant = HomeAssistant(coresys)
|
||||
coresys.addons = AddonManager(coresys)
|
||||
|
@ -6,7 +6,7 @@ from pathlib import Path, PurePath
|
||||
|
||||
from .const import (
|
||||
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
|
||||
ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_LAST_BOOT, ATTR_WAIT_BOOT)
|
||||
ATTR_LAST_BOOT, ATTR_WAIT_BOOT)
|
||||
from .utils.dt import parse_datetime
|
||||
from .utils.json import JsonConfig
|
||||
from .validate import SCHEMA_HASSIO_CONFIG
|
||||
@ -136,6 +136,11 @@ class CoreConfig(JsonConfig):
|
||||
"""Return hass.io temp folder."""
|
||||
return Path(HASSIO_DATA, TMP_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_tmp(self):
|
||||
"""Return hass.io temp folder for docker."""
|
||||
return PurePath(self.path_extern_hassio, TMP_DATA)
|
||||
|
||||
@property
|
||||
def path_backup(self):
|
||||
"""Return root backup data folder."""
|
||||
@ -174,23 +179,3 @@ class CoreConfig(JsonConfig):
|
||||
return
|
||||
|
||||
self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
|
||||
|
||||
@property
|
||||
def audio_output(self):
|
||||
"""Return ALSA audio output card,dev."""
|
||||
return self._data.get(ATTR_AUDIO_OUTPUT)
|
||||
|
||||
@audio_output.setter
|
||||
def audio_output(self, value):
|
||||
"""Set ALSA audio output card,dev."""
|
||||
self._data[ATTR_AUDIO_OUTPUT] = value
|
||||
|
||||
@property
|
||||
def audio_input(self):
|
||||
"""Return ALSA audio input card,dev."""
|
||||
return self._data.get(ATTR_AUDIO_INPUT)
|
||||
|
||||
@audio_input.setter
|
||||
def audio_input(self, value):
|
||||
"""Set ALSA audio input card,dev."""
|
||||
self._data[ATTR_AUDIO_INPUT] = value
|
||||
|
@ -27,6 +27,7 @@ DOCKER_NETWORK_RANGE = ip_network('172.30.33.0/24')
|
||||
LABEL_VERSION = 'io.hass.version'
|
||||
LABEL_ARCH = 'io.hass.arch'
|
||||
LABEL_TYPE = 'io.hass.type'
|
||||
LABEL_MACHINE = 'io.hass.machine'
|
||||
|
||||
META_ADDON = 'addon'
|
||||
META_SUPERVISOR = 'supervisor'
|
||||
@ -161,6 +162,8 @@ ATTR_CRYPTO = 'crypto'
|
||||
ATTR_BRANCH = 'branch'
|
||||
ATTR_SECCOMP = 'seccomp'
|
||||
ATTR_APPARMOR = 'apparmor'
|
||||
ATTR_CACHE = 'cache'
|
||||
ATTR_DEFAULT = 'default'
|
||||
|
||||
SERVICE_MQTT = 'mqtt'
|
||||
|
||||
|
@ -42,6 +42,7 @@ class CoreSys(object):
|
||||
self._snapshots = None
|
||||
self._tasks = None
|
||||
self._services = None
|
||||
self._audio = None
|
||||
|
||||
@property
|
||||
def arch(self):
|
||||
@ -50,6 +51,13 @@ class CoreSys(object):
|
||||
return self._supervisor.arch
|
||||
return None
|
||||
|
||||
@property
|
||||
def machine(self):
|
||||
"""Return running machine type of hass.io system."""
|
||||
if self._homeassistant:
|
||||
return self._homeassistant.machine
|
||||
return None
|
||||
|
||||
@property
|
||||
def dev(self):
|
||||
"""Return True if we run dev modus."""
|
||||
@ -196,6 +204,18 @@ class CoreSys(object):
|
||||
raise RuntimeError("Services already set!")
|
||||
self._services = value
|
||||
|
||||
@property
|
||||
def audio(self):
|
||||
"""Return ALSA Audio object."""
|
||||
return self._audio
|
||||
|
||||
@audio.setter
|
||||
def audio(self, value):
|
||||
"""Set a ALSA Audio object."""
|
||||
if self._audio:
|
||||
raise RuntimeError("Audio already set!")
|
||||
self._audio = value
|
||||
|
||||
|
||||
class CoreSysAttributes(object):
|
||||
"""Inheret basic CoreSysAttributes."""
|
||||
|
@ -201,7 +201,7 @@ class DockerAddon(DockerInterface):
|
||||
'bind': "/share", 'mode': addon_mapping[MAP_SHARE]
|
||||
}})
|
||||
|
||||
# init other hardware mappings
|
||||
# Init other hardware mappings
|
||||
if self.addon.with_gpio:
|
||||
volumes.update({
|
||||
"/sys/class/gpio": {
|
||||
@ -212,13 +212,20 @@ class DockerAddon(DockerInterface):
|
||||
},
|
||||
})
|
||||
|
||||
# host dbus system
|
||||
# Host dbus system
|
||||
if self.addon.host_dbus:
|
||||
volumes.update({
|
||||
"/var/run/dbus": {
|
||||
'bind': "/var/run/dbus", 'mode': 'rw'
|
||||
}})
|
||||
|
||||
# ALSA configuration
|
||||
if self.addon.with_audio:
|
||||
volumes.update({
|
||||
str(self.addon.path_extern_asound): {
|
||||
'bind': "/etc/asound.conf", 'mode': 'ro'
|
||||
}})
|
||||
|
||||
return volumes
|
||||
|
||||
def _run(self):
|
||||
|
@ -4,7 +4,7 @@ import logging
|
||||
import docker
|
||||
|
||||
from .interface import DockerInterface
|
||||
from ..const import ENV_TOKEN, ENV_TIME
|
||||
from ..const import ENV_TOKEN, ENV_TIME, LABEL_MACHINE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -14,6 +14,13 @@ HASS_DOCKER_NAME = 'homeassistant'
|
||||
class DockerHomeAssistant(DockerInterface):
|
||||
"""Docker hassio wrapper for HomeAssistant."""
|
||||
|
||||
@property
|
||||
def machine(self):
|
||||
"""Return machine of Home-Assistant docker image."""
|
||||
if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']:
|
||||
return self._meta['Config']['Labels'][LABEL_MACHINE]
|
||||
return None
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Return name of docker image."""
|
||||
|
@ -45,6 +45,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
|
||||
await self.install_landingpage()
|
||||
|
||||
@property
|
||||
def machine(self):
|
||||
"""Return System Machines."""
|
||||
return self._docker.machine
|
||||
|
||||
@property
|
||||
def api_ip(self):
|
||||
"""Return IP of HomeAssistant instance."""
|
||||
|
1
hassio/host/__init__.py
Normal file
1
hassio/host/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Host function like audio/dbus/systemd."""
|
17
hassio/host/asound.tmpl
Normal file
17
hassio/host/asound.tmpl
Normal file
@ -0,0 +1,17 @@
|
||||
pcm.!default {
|
||||
type asym
|
||||
capture.pcm "mic"
|
||||
playback.pcm "speaker"
|
||||
}
|
||||
pcm.mic {
|
||||
type plug
|
||||
slave {
|
||||
pcm "hw:{$input}"
|
||||
}
|
||||
}
|
||||
pcm.speaker {
|
||||
type plug
|
||||
slave {
|
||||
pcm "hw:{$output}"
|
||||
}
|
||||
}
|
114
hassio/host/audio.py
Normal file
114
hassio/host/audio.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Host Audio-support."""
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
from ..const import (
|
||||
ATTR_CACHE, ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME, ATTR_DEFAULT)
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DefaultConfig = namedtuple('DefaultConfig', ['input', 'output'])
|
||||
|
||||
|
||||
class AlsaAudio(CoreSysAttributes):
|
||||
"""Handle Audio ALSA host data."""
|
||||
|
||||
def __init__(self, coresys):
|
||||
"""Initialize Alsa audio system."""
|
||||
self.coresys = coresys
|
||||
self._data = {
|
||||
ATTR_CACHE: 0,
|
||||
ATTR_INPUT: {},
|
||||
ATTR_OUTPUT: {},
|
||||
}
|
||||
|
||||
@property
|
||||
def input_devices(self):
|
||||
"""Return list of ALSA input devices."""
|
||||
self._update_device()
|
||||
return self._data[ATTR_INPUT]
|
||||
|
||||
@property
|
||||
def output_devices(self):
|
||||
"""Return list of ALSA output devices."""
|
||||
self._update_device()
|
||||
return self._data[ATTR_OUTPUT]
|
||||
|
||||
def _update_device(self):
|
||||
"""Update Internal device DB."""
|
||||
current_id = hash(frozenset(self._hardware.audio_devices))
|
||||
|
||||
# Need rebuild?
|
||||
if current_id == self._data[ATTR_CACHE]:
|
||||
return
|
||||
|
||||
# Init database
|
||||
_LOGGER.info("Update ALSA device list")
|
||||
database = self._audio_database()
|
||||
|
||||
# Process devices
|
||||
for dev_id, dev_data in self._hardware.audio_devices.items():
|
||||
for chan_id, chan_type in dev_data[ATTR_DEVICES]:
|
||||
alsa_id = f"{dev_id},{chan_id}"
|
||||
if chan_type.endswith('playback'):
|
||||
key = ATTR_OUTPUT
|
||||
elif chan_type.endswith('capture'):
|
||||
key = ATTR_INPUT
|
||||
else:
|
||||
_LOGGER.warning("Unknown channel type: %s", chan_type)
|
||||
continue
|
||||
|
||||
self._data[key][alsa_id] = database.get(self._machine, {}).get(
|
||||
alsa_id, f"{dev_data[ATTR_NAME]}: {chan_id}")
|
||||
|
||||
self._data[ATTR_CACHE] = current_id
|
||||
|
||||
@staticmethod
|
||||
def _audio_database():
|
||||
"""Read local json audio data into dict."""
|
||||
json_file = Path(__file__).parent.joinpath('audiodb.json')
|
||||
|
||||
try:
|
||||
with json_file.open('r') as database:
|
||||
return json.loads(database.read())
|
||||
except (ValueError, OSError) as err:
|
||||
_LOGGER.warning("Can't read audio DB: %s", err)
|
||||
|
||||
return {}
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
"""Generate ALSA default setting."""
|
||||
if ATTR_DEFAULT in self._data:
|
||||
return self._data[ATTR_DEFAULT]
|
||||
|
||||
database = self._audio_database()
|
||||
alsa_input = database.get(self._machine, {}).get(ATTR_INPUT, "0,0")
|
||||
alsa_output = database.get(self._machine, {}).get(ATTR_OUTPUT, "0,0")
|
||||
|
||||
self._data[ATTR_DEFAULT] = DefaultConfig(alsa_input, alsa_output)
|
||||
return self._data[ATTR_DEFAULT]
|
||||
|
||||
def asound(self, alsa_input=None, alsa_output=None):
|
||||
"""Generate a asound data."""
|
||||
alsa_input = alsa_input or self.default.input
|
||||
alsa_output = alsa_output or self.default.output
|
||||
|
||||
# Read Template
|
||||
asound_file = Path(__file__).parent.joinpath('asound.tmpl')
|
||||
try:
|
||||
with asound_file.open('r') as asound:
|
||||
asound_data = asound.read()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't read asound.tmpl: %s", err)
|
||||
return ""
|
||||
|
||||
# Process Template
|
||||
asound_template = Template(asound_data)
|
||||
return asound_template.safe_substitute(
|
||||
input=alsa_input, output=alsa_output
|
||||
)
|
18
hassio/host/audiodb.json
Normal file
18
hassio/host/audiodb.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"raspberrypi3": {
|
||||
"bcm2835 - bcm2835 ALSA": {
|
||||
"0,0": "Jack",
|
||||
"0,1": "HDMI"
|
||||
},
|
||||
"output": "0,0",
|
||||
"input": "1,0"
|
||||
},
|
||||
"raspberrypi2": {
|
||||
"output": "0,0",
|
||||
"input": "1,0"
|
||||
},
|
||||
"raspberrypi": {
|
||||
"output": "0,0",
|
||||
"input": "1,0"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user