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`
|
- POST `/host/update`
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
@ -255,7 +246,11 @@ Optional:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- GET `/host/hardware`
|
- POST `/host/reload`
|
||||||
|
|
||||||
|
### Hardware
|
||||||
|
|
||||||
|
- GET `/hardware/info`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"serial": ["/dev/xy"],
|
"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
|
### Network
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for HassIO addons."""
|
||||||
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
@ -372,15 +373,14 @@ class Addon(CoreSysAttributes):
|
|||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
setting = self._config.audio_output
|
|
||||||
if self.is_installed and \
|
if self.is_installed and \
|
||||||
ATTR_AUDIO_OUTPUT in self._data.user[self._id]:
|
ATTR_AUDIO_OUTPUT in self._data.user[self._id]:
|
||||||
setting = self._data.user[self._id][ATTR_AUDIO_OUTPUT]
|
return self._data.user[self._id][ATTR_AUDIO_OUTPUT]
|
||||||
return setting
|
return self._alsa.default.output
|
||||||
|
|
||||||
@audio_output.setter
|
@audio_output.setter
|
||||||
def audio_output(self, value):
|
def audio_output(self, value):
|
||||||
"""Set/remove custom audio output settings."""
|
"""Set/reset audio output settings."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self._data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None)
|
self._data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None)
|
||||||
else:
|
else:
|
||||||
@ -392,14 +392,13 @@ class Addon(CoreSysAttributes):
|
|||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
setting = self._config.audio_input
|
|
||||||
if self.is_installed and ATTR_AUDIO_INPUT in self._data.user[self._id]:
|
if self.is_installed and ATTR_AUDIO_INPUT in self._data.user[self._id]:
|
||||||
setting = self._data.user[self._id][ATTR_AUDIO_INPUT]
|
return self._data.user[self._id][ATTR_AUDIO_INPUT]
|
||||||
return setting
|
return self._alsa.default.input
|
||||||
|
|
||||||
@audio_input.setter
|
@audio_input.setter
|
||||||
def audio_input(self, value):
|
def audio_input(self, value):
|
||||||
"""Set/remove custom audio input settings."""
|
"""Set/reset audio input settings."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self._data.user[self._id].pop(ATTR_AUDIO_INPUT, None)
|
self._data.user[self._id].pop(ATTR_AUDIO_INPUT, None)
|
||||||
else:
|
else:
|
||||||
@ -504,6 +503,16 @@ class Addon(CoreSysAttributes):
|
|||||||
"""Return path to custom AppArmor profile."""
|
"""Return path to custom AppArmor profile."""
|
||||||
return Path(self.path_location, 'apparmor')
|
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):
|
def save_data(self):
|
||||||
"""Save data of addon."""
|
"""Save data of addon."""
|
||||||
self._addons.data.save_data()
|
self._addons.data.save_data()
|
||||||
@ -526,6 +535,20 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
return False
|
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
|
@property
|
||||||
def schema(self):
|
def schema(self):
|
||||||
"""Create a schema for addon options."""
|
"""Create a schema for addon options."""
|
||||||
@ -598,6 +621,11 @@ class Addon(CoreSysAttributes):
|
|||||||
"Remove Home-Assistant addon data folder %s", self.path_data)
|
"Remove Home-Assistant addon data folder %s", self.path_data)
|
||||||
shutil.rmtree(str(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()
|
self._set_uninstall()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -613,9 +641,14 @@ class Addon(CoreSysAttributes):
|
|||||||
@check_installed
|
@check_installed
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Set options and start addon."""
|
"""Set options and start addon."""
|
||||||
|
# Options
|
||||||
if not self.write_options():
|
if not self.write_options():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Sound
|
||||||
|
if self.with_audio and not self.write_asound():
|
||||||
|
return False
|
||||||
|
|
||||||
return await self.instance.run()
|
return await self.instance.run()
|
||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
|
@ -19,7 +19,7 @@ from ..const import (
|
|||||||
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
|
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
|
||||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
||||||
ATTR_SECCOMP, ATTR_APPARMOR)
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -165,8 +165,8 @@ SCHEMA_ADDON_USER = vol.Schema({
|
|||||||
vol.Optional(ATTR_BOOT):
|
vol.Optional(ATTR_BOOT):
|
||||||
vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from aiohttp import web
|
|||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
|
from .hardware import APIHardware
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .network import APINetwork
|
from .network import APINetwork
|
||||||
from .proxy import APIProxy
|
from .proxy import APIProxy
|
||||||
@ -37,6 +38,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
self._register_supervisor()
|
self._register_supervisor()
|
||||||
self._register_host()
|
self._register_host()
|
||||||
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
self._register_panel()
|
self._register_panel()
|
||||||
@ -53,11 +55,9 @@ class RestAPI(CoreSysAttributes):
|
|||||||
|
|
||||||
self.webapp.add_routes([
|
self.webapp.add_routes([
|
||||||
web.get('/host/info', api_host.info),
|
web.get('/host/info', api_host.info),
|
||||||
web.get('/host/hardware', api_host.hardware),
|
|
||||||
web.post('/host/reboot', api_host.reboot),
|
web.post('/host/reboot', api_host.reboot),
|
||||||
web.post('/host/shutdown', api_host.shutdown),
|
web.post('/host/shutdown', api_host.shutdown),
|
||||||
web.post('/host/update', api_host.update),
|
web.post('/host/update', api_host.update),
|
||||||
web.post('/host/options', api_host.options),
|
|
||||||
web.post('/host/reload', api_host.reload),
|
web.post('/host/reload', api_host.reload),
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -71,6 +71,16 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post('/network/options', api_net.options),
|
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):
|
def _register_supervisor(self):
|
||||||
"""Register supervisor function."""
|
"""Register supervisor function."""
|
||||||
api_supervisor = APISupervisor()
|
api_supervisor = APISupervisor()
|
||||||
|
@ -20,7 +20,7 @@ from ..const import (
|
|||||||
ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR,
|
ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR,
|
||||||
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
|
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import DOCKER_PORTS
|
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
vol.Optional(ATTR_NETWORK): vol.Any(None, DOCKER_PORTS),
|
vol.Optional(ATTR_NETWORK): vol.Any(None, DOCKER_PORTS),
|
||||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
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 .utils import api_process_hostcontrol, api_process, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
|
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_OS)
|
||||||
ATTR_AUDIO_OUTPUT, ATTR_GPIO)
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import ALSA_CHANNEL
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -18,11 +16,6 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
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):
|
class APIHost(CoreSysAttributes):
|
||||||
"""Handle rest api for host functions."""
|
"""Handle rest api for host functions."""
|
||||||
@ -39,19 +32,6 @@ class APIHost(CoreSysAttributes):
|
|||||||
ATTR_OS: self._host_control.os_info,
|
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
|
@api_process_hostcontrol
|
||||||
def reboot(self, request):
|
def reboot(self, request):
|
||||||
"""Reboot host."""
|
"""Reboot host."""
|
||||||
@ -79,14 +59,3 @@ class APIHost(CoreSysAttributes):
|
|||||||
|
|
||||||
return await asyncio.shield(
|
return await asyncio.shield(
|
||||||
self._host_control.update(version=version), loop=self._loop)
|
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 .tasks import Tasks
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
from .services import ServiceManager
|
from .services import ServiceManager
|
||||||
|
from .host import AlsaAudio
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ def initialize_coresys(loop):
|
|||||||
# Initialize core objects
|
# Initialize core objects
|
||||||
coresys.updater = Updater(coresys)
|
coresys.updater = Updater(coresys)
|
||||||
coresys.api = RestAPI(coresys)
|
coresys.api = RestAPI(coresys)
|
||||||
|
coresys.alsa = AlsaAudio(coresys)
|
||||||
coresys.supervisor = Supervisor(coresys)
|
coresys.supervisor = Supervisor(coresys)
|
||||||
coresys.homeassistant = HomeAssistant(coresys)
|
coresys.homeassistant = HomeAssistant(coresys)
|
||||||
coresys.addons = AddonManager(coresys)
|
coresys.addons = AddonManager(coresys)
|
||||||
|
@ -6,7 +6,7 @@ from pathlib import Path, PurePath
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
|
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.dt import parse_datetime
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
from .validate import SCHEMA_HASSIO_CONFIG
|
from .validate import SCHEMA_HASSIO_CONFIG
|
||||||
@ -136,6 +136,11 @@ class CoreConfig(JsonConfig):
|
|||||||
"""Return hass.io temp folder."""
|
"""Return hass.io temp folder."""
|
||||||
return Path(HASSIO_DATA, TMP_DATA)
|
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
|
@property
|
||||||
def path_backup(self):
|
def path_backup(self):
|
||||||
"""Return root backup data folder."""
|
"""Return root backup data folder."""
|
||||||
@ -174,23 +179,3 @@ class CoreConfig(JsonConfig):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
|
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_VERSION = 'io.hass.version'
|
||||||
LABEL_ARCH = 'io.hass.arch'
|
LABEL_ARCH = 'io.hass.arch'
|
||||||
LABEL_TYPE = 'io.hass.type'
|
LABEL_TYPE = 'io.hass.type'
|
||||||
|
LABEL_MACHINE = 'io.hass.machine'
|
||||||
|
|
||||||
META_ADDON = 'addon'
|
META_ADDON = 'addon'
|
||||||
META_SUPERVISOR = 'supervisor'
|
META_SUPERVISOR = 'supervisor'
|
||||||
|
@ -42,6 +42,7 @@ class CoreSys(object):
|
|||||||
self._snapshots = None
|
self._snapshots = None
|
||||||
self._tasks = None
|
self._tasks = None
|
||||||
self._services = None
|
self._services = None
|
||||||
|
self._alsa = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
@ -50,6 +51,13 @@ class CoreSys(object):
|
|||||||
return self._supervisor.arch
|
return self._supervisor.arch
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def machine(self):
|
||||||
|
"""Return running machine type of hass.io system."""
|
||||||
|
if self._homeassistant:
|
||||||
|
return self._homeassistant.machine
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dev(self):
|
def dev(self):
|
||||||
"""Return True if we run dev modus."""
|
"""Return True if we run dev modus."""
|
||||||
@ -196,6 +204,18 @@ class CoreSys(object):
|
|||||||
raise RuntimeError("Services already set!")
|
raise RuntimeError("Services already set!")
|
||||||
self._services = value
|
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):
|
class CoreSysAttributes(object):
|
||||||
"""Inheret basic CoreSysAttributes."""
|
"""Inheret basic CoreSysAttributes."""
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""Init file for HassIO docker object."""
|
"""Init file for HassIO docker object."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from collections import namedtuple
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import attr
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
from .network import DockerNetwork
|
from .network import DockerNetwork
|
||||||
@ -10,7 +10,8 @@ from ..const import SOCKET_DOCKER
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
class DockerAPI(object):
|
||||||
|
@ -201,7 +201,7 @@ class DockerAddon(DockerInterface):
|
|||||||
'bind': "/share", 'mode': addon_mapping[MAP_SHARE]
|
'bind': "/share", 'mode': addon_mapping[MAP_SHARE]
|
||||||
}})
|
}})
|
||||||
|
|
||||||
# init other hardware mappings
|
# Init other hardware mappings
|
||||||
if self.addon.with_gpio:
|
if self.addon.with_gpio:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
"/sys/class/gpio": {
|
"/sys/class/gpio": {
|
||||||
@ -212,13 +212,20 @@ class DockerAddon(DockerInterface):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
# host dbus system
|
# Host dbus system
|
||||||
if self.addon.host_dbus:
|
if self.addon.host_dbus:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
"/var/run/dbus": {
|
"/var/run/dbus": {
|
||||||
'bind': "/var/run/dbus", 'mode': 'rw'
|
'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
|
return volumes
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
import docker
|
import docker
|
||||||
|
|
||||||
from .interface import DockerInterface
|
from .interface import DockerInterface
|
||||||
from ..const import ENV_TOKEN, ENV_TIME
|
from ..const import ENV_TOKEN, ENV_TIME, LABEL_MACHINE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -14,6 +14,13 @@ HASS_DOCKER_NAME = 'homeassistant'
|
|||||||
class DockerHomeAssistant(DockerInterface):
|
class DockerHomeAssistant(DockerInterface):
|
||||||
"""Docker hassio wrapper for HomeAssistant."""
|
"""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
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return name of docker image."""
|
"""Return name of docker image."""
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""HomeAssistant control object."""
|
"""HomeAssistant control object."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@ -9,6 +8,7 @@ import time
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.hdrs import CONTENT_TYPE
|
from aiohttp.hdrs import CONTENT_TYPE
|
||||||
|
import attr
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
|
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")
|
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):
|
class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||||
@ -45,6 +46,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
|
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
|
||||||
await self.install_landingpage()
|
await self.install_landingpage()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def machine(self):
|
||||||
|
"""Return System Machines."""
|
||||||
|
return self.instance.machine
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_ip(self):
|
def api_ip(self):
|
||||||
"""Return IP of HomeAssistant instance."""
|
"""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 (
|
from .const import (
|
||||||
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE,
|
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE,
|
||||||
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
|
||||||
ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT,
|
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG,
|
||||||
ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_UUID,
|
ATTR_WAIT_BOOT, ATTR_UUID, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
||||||
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
|
||||||
|
|
||||||
|
|
||||||
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||||
|
|
||||||
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
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))
|
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
|
||||||
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
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])
|
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=[
|
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[
|
||||||
"https://github.com/hassio-addons/repository",
|
"https://github.com/hassio-addons/repository",
|
||||||
]): REPOSITORIES,
|
]): REPOSITORIES,
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
|
||||||
vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT,
|
vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT,
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user