mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 13:46:31 +00:00
Merge pull request #441 from home-assistant/new_audio_system
Extend Audio support
This commit is contained in:
commit
35b3f364c9
30
API.md
30
API.md
@ -236,15 +236,6 @@ return:
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/host/options`
|
||||
|
||||
```json
|
||||
{
|
||||
"audio_input": "0,0",
|
||||
"audio_output": "0,0"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/host/update`
|
||||
|
||||
Optional:
|
||||
@ -255,7 +246,11 @@ Optional:
|
||||
}
|
||||
```
|
||||
|
||||
- GET `/host/hardware`
|
||||
- POST `/host/reload`
|
||||
|
||||
### Hardware
|
||||
|
||||
- GET `/hardware/info`
|
||||
```json
|
||||
{
|
||||
"serial": ["/dev/xy"],
|
||||
@ -274,7 +269,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._alsa.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._alsa.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._alsa.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."""
|
||||
@ -598,6 +621,11 @@ class Addon(CoreSysAttributes):
|
||||
"Remove Home-Assistant addon data folder %s", self.path_data)
|
||||
shutil.rmtree(str(self.path_data))
|
||||
|
||||
# Cleanup audio settings
|
||||
if self.path_asound.exists():
|
||||
with suppress(OSError):
|
||||
self.path_asound.unlink()
|
||||
|
||||
self._set_uninstall()
|
||||
return True
|
||||
|
||||
@ -613,9 +641,14 @@ 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
|
||||
|
@ -19,7 +19,7 @@ from ..const import (
|
||||
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
|
||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
||||
ATTR_SECCOMP, ATTR_APPARMOR)
|
||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
|
||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -165,8 +165,8 @@ SCHEMA_ADDON_USER = vol.Schema({
|
||||
vol.Optional(ATTR_BOOT):
|
||||
vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
|
||||
}, extra=vol.REMOVE_EXTRA)
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ from aiohttp import web
|
||||
from .addons import APIAddons
|
||||
from .discovery import APIDiscovery
|
||||
from .homeassistant import APIHomeAssistant
|
||||
from .hardware import APIHardware
|
||||
from .host import APIHost
|
||||
from .network import APINetwork
|
||||
from .proxy import APIProxy
|
||||
@ -37,6 +38,7 @@ class RestAPI(CoreSysAttributes):
|
||||
"""Register REST API Calls."""
|
||||
self._register_supervisor()
|
||||
self._register_host()
|
||||
self._register_hardware()
|
||||
self._register_homeassistant()
|
||||
self._register_proxy()
|
||||
self._register_panel()
|
||||
@ -53,11 +55,9 @@ class RestAPI(CoreSysAttributes):
|
||||
|
||||
self.webapp.add_routes([
|
||||
web.get('/host/info', api_host.info),
|
||||
web.get('/host/hardware', api_host.hardware),
|
||||
web.post('/host/reboot', api_host.reboot),
|
||||
web.post('/host/shutdown', api_host.shutdown),
|
||||
web.post('/host/update', api_host.update),
|
||||
web.post('/host/options', api_host.options),
|
||||
web.post('/host/reload', api_host.reload),
|
||||
])
|
||||
|
||||
@ -71,6 +71,16 @@ class RestAPI(CoreSysAttributes):
|
||||
web.post('/network/options', api_net.options),
|
||||
])
|
||||
|
||||
def _register_hardware(self):
|
||||
"""Register hardware function."""
|
||||
api_hardware = APIHardware()
|
||||
api_hardware.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes([
|
||||
web.get('/hardware/info', api_hardware.info),
|
||||
web.get('/hardware/audio', api_hardware.audio),
|
||||
])
|
||||
|
||||
def _register_supervisor(self):
|
||||
"""Register supervisor function."""
|
||||
api_supervisor = APISupervisor()
|
||||
|
@ -20,7 +20,7 @@ from ..const import (
|
||||
ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR,
|
||||
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..validate import DOCKER_PORTS
|
||||
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -33,6 +33,8 @@ SCHEMA_OPTIONS = vol.Schema({
|
||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||
vol.Optional(ATTR_NETWORK): vol.Any(None, DOCKER_PORTS),
|
||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
|
||||
})
|
||||
|
||||
|
||||
|
34
hassio/api/hardware.py
Normal file
34
hassio/api/hardware.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Init file for HassIO hardware rest api."""
|
||||
import logging
|
||||
|
||||
from .utils import api_process
|
||||
from ..const import (
|
||||
ATTR_SERIAL, ATTR_DISK, ATTR_GPIO, ATTR_AUDIO, ATTR_INPUT, ATTR_OUTPUT)
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIHardware(CoreSysAttributes):
|
||||
"""Handle rest api for hardware functions."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Show hardware info."""
|
||||
return {
|
||||
ATTR_SERIAL: list(self._hardware.serial_devices),
|
||||
ATTR_INPUT: list(self._hardware.input_devices),
|
||||
ATTR_DISK: list(self._hardware.disk_devices),
|
||||
ATTR_GPIO: list(self._hardware.gpio_devices),
|
||||
ATTR_AUDIO: self._hardware.audio_devices,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def audio(self, request):
|
||||
"""Show ALSA audio devices."""
|
||||
return {
|
||||
ATTR_AUDIO: {
|
||||
ATTR_INPUT: self._alsa.input_devices,
|
||||
ATTR_OUTPUT: self._alsa.output_devices,
|
||||
}
|
||||
}
|
@ -7,10 +7,8 @@ import voluptuous as vol
|
||||
from .utils import api_process_hostcontrol, api_process, api_validate
|
||||
from ..const import (
|
||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
|
||||
ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
||||
ATTR_AUDIO_OUTPUT, ATTR_GPIO)
|
||||
ATTR_OS)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..validate import ALSA_CHANNEL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -18,11 +16,6 @@ SCHEMA_VERSION = vol.Schema({
|
||||
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
||||
})
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema({
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
||||
})
|
||||
|
||||
|
||||
class APIHost(CoreSysAttributes):
|
||||
"""Handle rest api for host functions."""
|
||||
@ -39,19 +32,6 @@ class APIHost(CoreSysAttributes):
|
||||
ATTR_OS: self._host_control.os_info,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def options(self, request):
|
||||
"""Process host options."""
|
||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||
|
||||
if ATTR_AUDIO_OUTPUT in body:
|
||||
self._config.audio_output = body[ATTR_AUDIO_OUTPUT]
|
||||
if ATTR_AUDIO_INPUT in body:
|
||||
self._config.audio_input = body[ATTR_AUDIO_INPUT]
|
||||
|
||||
self._config.save_data()
|
||||
return True
|
||||
|
||||
@api_process_hostcontrol
|
||||
def reboot(self, request):
|
||||
"""Reboot host."""
|
||||
@ -79,14 +59,3 @@ class APIHost(CoreSysAttributes):
|
||||
|
||||
return await asyncio.shield(
|
||||
self._host_control.update(version=version), loop=self._loop)
|
||||
|
||||
@api_process
|
||||
async def hardware(self, request):
|
||||
"""Return local hardware infos."""
|
||||
return {
|
||||
ATTR_SERIAL: list(self._hardware.serial_devices),
|
||||
ATTR_INPUT: list(self._hardware.input_devices),
|
||||
ATTR_DISK: list(self._hardware.disk_devices),
|
||||
ATTR_GPIO: list(self._hardware.gpio_devices),
|
||||
ATTR_AUDIO: self._hardware.audio_devices,
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ from .snapshots import SnapshotManager
|
||||
from .tasks import Tasks
|
||||
from .updater import Updater
|
||||
from .services import ServiceManager
|
||||
from .host import AlsaAudio
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -28,6 +29,7 @@ def initialize_coresys(loop):
|
||||
# Initialize core objects
|
||||
coresys.updater = Updater(coresys)
|
||||
coresys.api = RestAPI(coresys)
|
||||
coresys.alsa = 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'
|
||||
|
@ -42,6 +42,7 @@ class CoreSys(object):
|
||||
self._snapshots = None
|
||||
self._tasks = None
|
||||
self._services = None
|
||||
self._alsa = 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 alsa(self):
|
||||
"""Return ALSA Audio object."""
|
||||
return self._alsa
|
||||
|
||||
@alsa.setter
|
||||
def alsa(self, value):
|
||||
"""Set a ALSA Audio object."""
|
||||
if self._alsa:
|
||||
raise RuntimeError("ALSA already set!")
|
||||
self._alsa = value
|
||||
|
||||
|
||||
class CoreSysAttributes(object):
|
||||
"""Inheret basic CoreSysAttributes."""
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Init file for HassIO docker object."""
|
||||
from contextlib import suppress
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
import attr
|
||||
import docker
|
||||
|
||||
from .network import DockerNetwork
|
||||
@ -10,7 +10,8 @@ from ..const import SOCKET_DOCKER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CommandReturn = namedtuple('CommandReturn', ['exit_code', 'output'])
|
||||
# pylint: disable=invalid-name
|
||||
CommandReturn = attr.make_class('CommandReturn', ['exit_code', 'output'])
|
||||
|
||||
|
||||
class DockerAPI(object):
|
||||
|
@ -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."""
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""HomeAssistant control object."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -9,6 +8,7 @@ import time
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import attr
|
||||
|
||||
from .const import (
|
||||
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
|
||||
@ -24,7 +24,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||
|
||||
ConfigResult = namedtuple('ConfigResult', ['valid', 'log'])
|
||||
# pylint: disable=invalid-name
|
||||
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'])
|
||||
|
||||
|
||||
class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
@ -45,6 +46,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.instance.machine
|
||||
|
||||
@property
|
||||
def api_ip(self):
|
||||
"""Return IP of HomeAssistant instance."""
|
||||
|
2
hassio/host/__init__.py
Normal file
2
hassio/host/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Host function like audio/dbus/systemd."""
|
||||
from .alsa import AlsaAudio # noqa
|
137
hassio/host/alsa.py
Normal file
137
hassio/host/alsa.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Host Audio-support."""
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
import attr
|
||||
|
||||
from ..const import ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
DefaultConfig = attr.make_class('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_INPUT: {},
|
||||
ATTR_OUTPUT: {},
|
||||
}
|
||||
self._cache = 0
|
||||
self._default = None
|
||||
|
||||
@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._cache:
|
||||
return
|
||||
|
||||
# Clean old stuff
|
||||
self._data[ATTR_INPUT].clear()
|
||||
self._data[ATTR_OUTPUT].clear()
|
||||
|
||||
# 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].items():
|
||||
alsa_id = f"{dev_id},{chan_id}"
|
||||
dev_name = dev_data[ATTR_NAME]
|
||||
|
||||
# Lookup type
|
||||
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
|
||||
|
||||
# Use name from DB or a generic name
|
||||
self._data[key][alsa_id] = database.get(
|
||||
self._machine, {}).get(
|
||||
dev_name, {}).get(alsa_id, f"{dev_name}: {chan_id}")
|
||||
|
||||
self._cache = current_id
|
||||
|
||||
@staticmethod
|
||||
def _audio_database():
|
||||
"""Read local json audio data into dict."""
|
||||
json_file = Path(__file__).parent.joinpath('audiodb.json')
|
||||
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
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."""
|
||||
# Init defaults
|
||||
if self._default is None:
|
||||
database = self._audio_database()
|
||||
alsa_input = database.get(self._machine, {}).get(ATTR_INPUT)
|
||||
alsa_output = database.get(self._machine, {}).get(ATTR_OUTPUT)
|
||||
|
||||
self._default = DefaultConfig(alsa_input, alsa_output)
|
||||
|
||||
# Search exists/new output
|
||||
if self._default.output is None and self.output_devices:
|
||||
self._default.output = next(iter(self.output_devices))
|
||||
_LOGGER.info("Detect output device %s", self._default.output)
|
||||
|
||||
# Search exists/new input
|
||||
if self._default.input is None and self.input_devices:
|
||||
self._default.input = next(iter(self.input_devices))
|
||||
_LOGGER.info("Detect input device %s", self._default.input)
|
||||
|
||||
return self._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:
|
||||
# pylint: disable=no-member
|
||||
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
|
||||
)
|
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}"
|
||||
}
|
||||
}
|
18
hassio/host/audiodb.json
Normal file
18
hassio/host/audiodb.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"raspberrypi3": {
|
||||
"bcm2835 - bcm2835 ALSA": {
|
||||
"0,0": "Raspberry Jack",
|
||||
"0,1": "Raspberry HDMI"
|
||||
},
|
||||
"output": "0,0",
|
||||
"input": null
|
||||
},
|
||||
"raspberrypi2": {
|
||||
"output": "0,0",
|
||||
"input": null
|
||||
},
|
||||
"raspberrypi": {
|
||||
"output": "0,0",
|
||||
"input": null
|
||||
}
|
||||
}
|
@ -7,18 +7,17 @@ import pytz
|
||||
|
||||
from .const import (
|
||||
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE,
|
||||
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
||||
ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT,
|
||||
ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_UUID,
|
||||
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
||||
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
|
||||
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG,
|
||||
ATTR_WAIT_BOOT, ATTR_UUID, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
||||
|
||||
|
||||
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||
|
||||
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
||||
ALSA_CHANNEL = vol.Match(r"\d+,\d+")
|
||||
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
|
||||
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
||||
ALSA_DEVICE = vol.Any(None, vol.Match(r"\d+,\d+"))
|
||||
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
|
||||
|
||||
|
||||
@ -110,7 +109,5 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({
|
||||
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[
|
||||
"https://github.com/hassio-addons/repository",
|
||||
]): REPOSITORIES,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
||||
vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT,
|
||||
}, extra=vol.REMOVE_EXTRA)
|
||||
|
Loading…
x
Reference in New Issue
Block a user