diff --git a/API.md b/API.md index 8e61cdf63..63a954591 100644 --- a/API.md +++ b/API.md @@ -379,7 +379,9 @@ Trigger an udev reload "port": 8123, "ssl": "bool", "watchdog": "bool", - "wait_boot": 600 + "wait_boot": 600, + "audio_input": "null|profile", + "audio_output": "null|profile" } ``` @@ -413,7 +415,9 @@ Output is the raw Docker log. "ssl": "bool", "refresh_token": "", "watchdog": "bool", - "wait_boot": 600 + "wait_boot": 600, + "audio_input": "null|profile", + "audio_output": "null|profile" } ``` diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index b2944803d..03a2ba3b0 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -294,11 +294,8 @@ class Addon(AddonModel): @audio_output.setter def audio_output(self, value: Optional[str]): - """Set/reset audio output profile settings.""" - if value is None: - self.persist.pop(ATTR_AUDIO_OUTPUT, None) - else: - self.persist[ATTR_AUDIO_OUTPUT] = value + """Set audio output profile settings.""" + self.persist[ATTR_AUDIO_OUTPUT] = value @property def audio_input(self) -> Optional[str]: @@ -315,11 +312,8 @@ class Addon(AddonModel): @audio_input.setter def audio_input(self, value: Optional[str]): - """Set/reset audio input settings.""" - if value is None: - self.persist.pop(ATTR_AUDIO_INPUT, None) - else: - self.persist[ATTR_AUDIO_INPUT] = value + """Set audio input settings.""" + self.persist[ATTR_AUDIO_INPUT] = value @property def image(self): diff --git a/supervisor/api/homeassistant.py b/supervisor/api/homeassistant.py index d9f691f96..f7e6821cc 100644 --- a/supervisor/api/homeassistant.py +++ b/supervisor/api/homeassistant.py @@ -1,24 +1,27 @@ """Init file for Supervisor Home Assistant RESTful API.""" import asyncio import logging -from typing import Coroutine, Dict, Any +from typing import Any, Coroutine, Dict -import voluptuous as vol from aiohttp import web +import voluptuous as vol from ..const import ( ATTR_ARCH, + ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_BOOT, ATTR_CPU_PERCENT, ATTR_CUSTOM, ATTR_IMAGE, + ATTR_IP_ADDRESS, ATTR_LAST_VERSION, ATTR_MACHINE, ATTR_MEMORY_LIMIT, - ATTR_MEMORY_USAGE, ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_PORT, @@ -27,7 +30,6 @@ from ..const import ( ATTR_VERSION, ATTR_WAIT_BOOT, ATTR_WATCHDOG, - ATTR_IP_ADDRESS, CONTENT_TYPE_BINARY, ) from ..coresys import CoreSysAttributes @@ -48,6 +50,8 @@ SCHEMA_OPTIONS = vol.Schema( vol.Optional(ATTR_WATCHDOG): vol.Boolean(), vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)), vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)), } ) @@ -73,6 +77,8 @@ class APIHomeAssistant(CoreSysAttributes): ATTR_SSL: self.sys_homeassistant.api_ssl, ATTR_WATCHDOG: self.sys_homeassistant.watchdog, ATTR_WAIT_BOOT: self.sys_homeassistant.wait_boot, + ATTR_AUDIO_INPUT: self.sys_homeassistant.audio_input, + ATTR_AUDIO_OUTPUT: self.sys_homeassistant.audio_output, } @api_process @@ -102,6 +108,12 @@ class APIHomeAssistant(CoreSysAttributes): if ATTR_REFRESH_TOKEN in body: self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN] + if ATTR_AUDIO_INPUT in body: + self.sys_homeassistant.audio_input = body[ATTR_AUDIO_INPUT] + + if ATTR_AUDIO_OUTPUT in body: + self.sys_homeassistant.audio_output = body[ATTR_AUDIO_OUTPUT] + self.sys_homeassistant.save_data() @api_process diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index 9be6ff75d..a8e27d650 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -213,10 +213,10 @@ class DockerAddon(DockerInterface): @property def volumes(self) -> Dict[str, Dict[str, str]]: """Generate volumes for mappings.""" - volumes = {str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"}} - addon_mapping = self.addon.map_volumes + volumes = {str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"}} + # setup config mappings if MAP_CONFIG in addon_mapping: volumes.update( diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 9bc909719..b5d55f100 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -2,7 +2,7 @@ from contextlib import suppress from ipaddress import IPv4Address import logging -from typing import Awaitable, Optional +from typing import Awaitable, Dict, Optional import docker @@ -45,6 +45,46 @@ class DockerHomeAssistant(DockerInterface): """Return IP address of this container.""" return self.sys_docker.network.gateway + @property + def volumes(self) -> Dict[str, Dict[str, str]]: + """Return Volumes for the mount.""" + volumes = {} + + # Add folders + volumes.update( + { + str(self.sys_config.path_extern_homeassistant): { + "bind": "/config", + "mode": "rw", + }, + str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"}, + str(self.sys_config.path_extern_share): { + "bind": "/share", + "mode": "rw", + }, + } + ) + + # Configuration Audio + volumes.update( + { + str(self.sys_homeassistant.path_extern_pulse): { + "bind": "/etc/pulse/client.conf", + "mode": "ro", + }, + str(self.sys_audio.path_extern_pulse): { + "bind": "/run/audio", + "mode": "ro", + }, + str(self.sys_audio.path_extern_asound): { + "bind": "/etc/asound.conf", + "mode": "ro", + }, + } + ) + + return volumes + def _run(self) -> None: """Run Docker image. @@ -67,6 +107,7 @@ class DockerHomeAssistant(DockerInterface): privileged=True, init=False, network_mode="host", + volumes=self.volumes, environment={ "HASSIO": self.sys_docker.network.supervisor, "SUPERVISOR": self.sys_docker.network.supervisor, @@ -74,17 +115,6 @@ class DockerHomeAssistant(DockerInterface): ENV_TOKEN: self.sys_homeassistant.hassio_token, ENV_TOKEN_OLD: self.sys_homeassistant.hassio_token, }, - volumes={ - str(self.sys_config.path_extern_homeassistant): { - "bind": "/config", - "mode": "rw", - }, - str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"}, - str(self.sys_config.path_extern_share): { - "bind": "/share", - "mode": "rw", - }, - }, ) self._meta = docker_container.attrs @@ -105,18 +135,8 @@ class DockerHomeAssistant(DockerInterface): detach=True, stdout=True, stderr=True, + volumes=self.volumes, environment={ENV_TIME: self.sys_timezone}, - volumes={ - str(self.sys_config.path_extern_homeassistant): { - "bind": "/config", - "mode": "rw", - }, - str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"}, - str(self.sys_config.path_extern_share): { - "bind": "/share", - "mode": "ro", - }, - }, ) def is_initialize(self) -> Awaitable[bool]: diff --git a/supervisor/homeassistant.py b/supervisor/homeassistant.py index b10e3e0ea..2f44e663f 100644 --- a/supervisor/homeassistant.py +++ b/supervisor/homeassistant.py @@ -19,6 +19,8 @@ from packaging import version as pkg_version from .const import ( ATTR_ACCESS_TOKEN, + ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_BOOT, ATTR_IMAGE, ATTR_LAST_VERSION, @@ -232,6 +234,36 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): """Set Home Assistant refresh_token.""" self._data[ATTR_REFRESH_TOKEN] = value + @property + def path_pulse(self): + """Return path to asound config.""" + return Path(self.sys_config.path_tmp, f"homeassistant_pulse") + + @property + def path_extern_pulse(self): + """Return path to asound config for Docker.""" + return Path(self.sys_config.path_extern_tmp, f"homeassistant_pulse") + + @property + def audio_output(self) -> Optional[str]: + """Return a pulse profile for output or None.""" + return self._data[ATTR_AUDIO_OUTPUT] + + @audio_output.setter + def audio_output(self, value: Optional[str]): + """Set audio output profile settings.""" + self._data[ATTR_AUDIO_OUTPUT] = value + + @property + def audio_input(self) -> Optional[str]: + """Return pulse profile for input or None.""" + return self._data[ATTR_AUDIO_INPUT] + + @audio_input.setter + def audio_input(self, value: Optional[str]): + """Set audio input settings.""" + self._data[ATTR_AUDIO_INPUT] = value + @process_lock async def install_landingpage(self) -> None: """Install a landing page.""" @@ -334,6 +366,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56) self.save_data() + # Write audio settings + self.write_pulse() + try: await self.instance.run() except DockerAPIError: @@ -602,3 +637,18 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): await self.instance.install(self.version) except DockerAPIError: _LOGGER.error("Repairing of Home Assistant fails") + + def write_pulse(self): + """Write asound config to file and return True on success.""" + pulse_config = self.sys_audio.pulse_client( + input_profile=self.audio_input, output_profile=self.audio_output + ) + + try: + with self.path_pulse.open("w") as config_file: + config_file.write(pulse_config) + except OSError as err: + _LOGGER.error("Home Assistant can't write pulse/client.config: %s", err) + raise HomeAssistantError() + + _LOGGER.debug("Home Assistant write pulse/client.config: %s", self.path_pulse) diff --git a/supervisor/snapshots/snapshot.py b/supervisor/snapshots/snapshot.py index 8b2084707..80c29ae6b 100644 --- a/supervisor/snapshots/snapshot.py +++ b/supervisor/snapshots/snapshot.py @@ -16,6 +16,8 @@ from voluptuous.humanize import humanize_error from ..const import ( ATTR_ADDONS, + ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_BOOT, ATTR_CRYPTO, ATTR_DATE, @@ -443,6 +445,10 @@ class Snapshot(CoreSysAttributes): self.sys_homeassistant.refresh_token ) + # Audio + self.homeassistant[ATTR_AUDIO_INPUT] = self.sys_homeassistant.audio_input + self.homeassistant[ATTR_AUDIO_OUTPUT] = self.sys_homeassistant.audio_output + def restore_homeassistant(self): """Write all data to the Home Assistant object.""" self.sys_homeassistant.watchdog = self.homeassistant[ATTR_WATCHDOG] @@ -463,6 +469,10 @@ class Snapshot(CoreSysAttributes): self.homeassistant[ATTR_REFRESH_TOKEN] ) + # Audio + self.sys_homeassistant.audio_input = self.homeassistant[ATTR_AUDIO_INPUT] + self.sys_homeassistant.audio_output = self.homeassistant[ATTR_AUDIO_OUTPUT] + # save self.sys_homeassistant.save_data() diff --git a/supervisor/snapshots/validate.py b/supervisor/snapshots/validate.py index 33ab4ea18..c52e486a9 100644 --- a/supervisor/snapshots/validate.py +++ b/supervisor/snapshots/validate.py @@ -3,6 +3,8 @@ import voluptuous as vol from ..const import ( ATTR_ADDONS, + ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_BOOT, ATTR_CRYPTO, ATTR_DATE, @@ -68,6 +70,12 @@ SCHEMA_SNAPSHOT = vol.Schema( vol.Optional(ATTR_WAIT_BOOT, default=600): vol.All( vol.Coerce(int), vol.Range(min=60) ), + vol.Optional(ATTR_AUDIO_OUTPUT, default=None): vol.Maybe( + vol.Coerce(str) + ), + vol.Optional(ATTR_AUDIO_INPUT, default=None): vol.Maybe( + vol.Coerce(str) + ), }, extra=vol.REMOVE_EXTRA, ), diff --git a/supervisor/validate.py b/supervisor/validate.py index 766a32d5e..7fb1df5b5 100644 --- a/supervisor/validate.py +++ b/supervisor/validate.py @@ -9,6 +9,8 @@ from .const import ( ATTR_ACCESS_TOKEN, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO, + ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_BOOT, ATTR_CHANNEL, ATTR_CLI, @@ -111,6 +113,8 @@ SCHEMA_HASS_CONFIG = vol.Schema( vol.Optional(ATTR_WAIT_BOOT, default=600): vol.All( vol.Coerce(int), vol.Range(min=60) ), + vol.Optional(ATTR_AUDIO_OUTPUT, default=None): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_AUDIO_INPUT, default=None): vol.Maybe(vol.Coerce(str)), }, extra=vol.REMOVE_EXTRA, )