mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-04-19 10:47:15 +00:00
Add Audio layer / PulseAudio (#1523)
* Improve alsa handling * use default from image * create alsa folder * Map config into addon * Add Audio object * Fix dbus * add host group file * Fix persistent file * Use new template * fix lint * Fix lint * add API * Update new base image / build system * Add audio container * extend new audio settings * provide pulse client config * Adjust files * Use without auth * reset did not exists now * cleanup old alsa layer * fix tasks * fix black * fix lint * Add dbus support * add dbus adjustments * Fixups
This commit is contained in:
parent
a3096153ab
commit
0212d027fb
@ -33,6 +33,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
containerd.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
jq \
|
||||
dbus \
|
||||
network-manager \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies from requirements.txt if it exists
|
||||
COPY requirements.txt requirements_tests.txt ./
|
||||
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
|
||||
|
@ -14,10 +14,10 @@
|
||||
# virtualenv
|
||||
venv/
|
||||
|
||||
# HA
|
||||
home-assistant-polymer/*
|
||||
misc/*
|
||||
script/*
|
||||
# Data
|
||||
home-assistant-polymer/
|
||||
script/
|
||||
tests/
|
||||
|
||||
# Test ENV
|
||||
data/
|
||||
|
39
API.md
39
API.md
@ -853,6 +853,45 @@ return:
|
||||
}
|
||||
```
|
||||
|
||||
### Audio
|
||||
|
||||
- GET `/audio/info`
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "ip-address",
|
||||
"version": "1",
|
||||
"latest_version": "2"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/update`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "VERSION"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/restart`
|
||||
|
||||
- GET `/audio/logs`
|
||||
|
||||
- GET `/audio/stats`
|
||||
|
||||
```json
|
||||
{
|
||||
"cpu_percent": 0.0,
|
||||
"memory_usage": 283123,
|
||||
"memory_limit": 329392,
|
||||
"memory_percent": 1.4,
|
||||
"network_tx": 0,
|
||||
"network_rx": 0,
|
||||
"blk_read": 0,
|
||||
"blk_write": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Auth / SSO API
|
||||
|
||||
You can use the user system on homeassistant. We handle this auth system on
|
||||
|
14
Dockerfile
14
Dockerfile
@ -23,15 +23,11 @@ RUN export MAKEFLAGS="-j$(nproc)" \
|
||||
-r ./requirements.txt \
|
||||
&& rm -f requirements.txt
|
||||
|
||||
# Install HassIO
|
||||
COPY . hassio
|
||||
RUN pip3 install --no-cache-dir -e ./hassio \
|
||||
&& python3 -m compileall ./hassio/hassio
|
||||
# Install Home Assistant Supervisor
|
||||
COPY . supervisor
|
||||
RUN pip3 install --no-cache-dir -e ./supervisor \
|
||||
&& python3 -m compileall ./supervisor/supervisor
|
||||
|
||||
|
||||
# Initialize udev daemon, handle CMD
|
||||
COPY entry.sh /bin/
|
||||
ENTRYPOINT ["/bin/entry.sh"]
|
||||
|
||||
WORKDIR /
|
||||
CMD [ "python3", "-m", "supervisor" ]
|
||||
COPY rootfs /
|
||||
|
@ -1,3 +1,3 @@
|
||||
include LICENSE.md
|
||||
graft hassio
|
||||
graft supervisor
|
||||
recursive-exclude * *.py[co]
|
||||
|
@ -10,8 +10,6 @@ communicates with the Supervisor. The Supervisor provides an API to manage the
|
||||
installation. This includes changing network settings or installing
|
||||
and updating software.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Installation instructions can be found at <https://home-assistant.io/hassio>.
|
||||
|
@ -10,10 +10,8 @@ trigger:
|
||||
- "*"
|
||||
pr: none
|
||||
variables:
|
||||
- name: basePythonTag
|
||||
value: "3.7-alpine3.11"
|
||||
- name: versionBuilder
|
||||
value: "6.9"
|
||||
value: "7.0"
|
||||
- group: docker
|
||||
|
||||
jobs:
|
||||
@ -51,6 +49,5 @@ jobs:
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw -v $(pwd):/data:ro \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--supervisor $(basePythonTag) --version $(Build.SourceBranchName) \
|
||||
--all -t /data --docker-hub homeassistant
|
||||
--generic $(Build.SourceBranchName) --all -t /data
|
||||
displayName: "Build Release"
|
||||
|
13
build.json
Normal file
13
build.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||
"build_from": {
|
||||
"aarch64": "homeassistant/aarch64-base-python:3.7-alpine3.11",
|
||||
"armhf": "homeassistant/armhf-base-python:3.7-alpine3.11",
|
||||
"armv7": "homeassistant/armv7-base-python:3.7-alpine3.11",
|
||||
"amd64": "homeassistant/amd64-base-python:3.7-alpine3.11",
|
||||
"i386": "homeassistant/i386-base-python:3.7-alpine3.11"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "supervisor"
|
||||
}
|
||||
}
|
BIN
misc/hassio.png
BIN
misc/hassio.png
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
@ -1 +0,0 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" version="7.9.5" editor="www.draw.io" type="device"><diagram name="Page-1" id="535f6c39-9b73-04c2-941c-82630de90f1a">5VrLcqM4FP0aLzsFiOcycefRVTPVqfFippdYKLYmMvII4cd8/QiQDEKQ4Bicnmp7Yevqybk691zJnoH55vDI4u36d5ogMnOs5DADX2eOY1vAER+F5VhZ3DCoDCuGE9moNizwv0j1lNYcJyjTGnJKCcdb3QhpmiLINVvMGN3rzV4o0WfdxitkGBYwJqb1T5zwtbTaflRXPCG8WsupQ8evKpYxfF0xmqdyvpkDXspXVb2J1VjyQbN1nNB9wwTuZ2DOKOXVt81hjkiBrYKt6vfQU3taN0MpH9JB+mkXkxypFftEdL17oWIEsUB+lKD4/+RUVXzJSpfdigZitoP4EBUl0KJuL4EpalPKNjGpO4tvq+Lz+0LNI9ZWTVVVSFhOszr7NeZosY1hUd6L7SYarfmGiJJdrAYTMqeEsrK1ABv5EAp7xhl9RY0aq3zJ9S/k+B14SdMOMY4ODZPE7xHRDeLsKJqo2ghUXeRe9yLp2329c1wF9LqxaXzZLpabdXUaunaY+CJ91u0/YPjvW4oLvy2OGUebC9GECVqGyy40gQ8ikJz8NS6AwUAAnREAdA0Av1L4itilyEHkCdJfGznXRM7pQg6MgJxvIPc0N1ArQyEqehTUO5PLIUTdXF6GnutZ42Do+p6OoW9i6FkmhN4IEEYGXigROiSLlPE1XdE0Jve19U5HtIEeOmD+V2G+CTxZ/KGqUrGwqs5TxR9yhL8R50epwHHOqTDVE/9G6VaO0Qt1RnMG5fKlyvOYrRDXtknxYG+6gyESc7zTBfgScFUuMTa6zhvoRiLxaeFbFp4Rw+IBELsS6O5ngR705hPLWuHPSzBsv0gw2gnEIt8itsOZCAlqAqbqnuIs+/a9N8E4mZe9SUe9Dez3w5YRnuZz369SDT2gJR4KE3ecsAU8PWyBjqzDDjvilj2GatrOFNyyG8RSUezELY1XZRgbSqJMMIPfFqcCYYBEbA4MlfkBE7WKQVyz1WmkQbbgs8gGpolwmhd0J7Tkoy62A9xAzIe6EKWJOZgwNobqTPjn80sc64Sfpl0qHjSSKzHKl1vx6ALDIppdJ2LFKHyBYyWresRyOtL8U3DS0nx3jIjlX5kr9o2l5wI3dhhemg8MpFWDLilNkcaVN9NmjRHAZITal9dnhDuJ4kifNZK5kRAe7tC+awqYs92Jzx922Kdpk2veTHzAgRoIvd4832d9InK52zrx/rjrrqE1pqduk4SmmeGvbB1vi69bRiHKsvd1RhelwarzIF6lcleHAMFSy/EDEDnA90InDC0XTJRFd2mSY3umJkUjSJK6vJsypNWltuRcmtTJsNck2Sgn2/FClez6THF50JQuV2ei9rlJjVDRUnZyGjfnZ45TUdkYp9wUp6cZtk9Ck6CQU/OKUvEz35CqAbgrqIChQD5eIvJMM8wxTUWTJeWcbkQDUlTcnX610K7Sy98t6jFuCV4VfTk9j+b1zXv7rl5OMAKRW5d4oOMSD3SklqNcwZs0HkBSK9BY6r7HUtvk6BA6XkXzztTxQYqofkH8KZIZtZgGA/f7vRm9CcHbrHSDZCIkNE8u1smrECjS45lrdZzOgqnuk8DbN+Fyc3/gOHYmRybK5RtaW58Bq0U6vWo7jCauSRO1WydXUre1ZdrRdDwJBP0/01lP+bJXCWHMLqefX7466OcV73HoF4FWOtFFv67r3FEULJiIfc19H4yZZU5P2WHs867BvsFu9AySPGK+npoefeqE7MRDwTT0cNWh9Sr0CH8VcYp8naPBZdrk/xraZP4R4g+0LY5alGHUf4vy/yWfusifgHyiWP/5rXJG/Q9DcP8f</diagram></mxfile>
|
9
rootfs/etc/cont-init.d/udev.sh
Normal file
9
rootfs/etc/cont-init.d/udev.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Start udev service
|
||||
# ==============================================================================
|
||||
udevd --daemon
|
||||
|
||||
bashio::log.info "Update udev informations"
|
||||
udevadm trigger
|
||||
udevadm settle
|
5
rootfs/etc/services.d/supervisor/finish
Normal file
5
rootfs/etc/services.d/supervisor/finish
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/execlineb -S0
|
||||
# ==============================================================================
|
||||
# Take down the S6 supervision tree when Supervisor fails
|
||||
# ==============================================================================
|
||||
s6-svscanctl -t /var/run/s6/services
|
5
rootfs/etc/services.d/supervisor/run
Normal file
5
rootfs/etc/services.d/supervisor/run
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Start Service service
|
||||
# ==============================================================================
|
||||
exec python3 -m supervisor
|
@ -61,9 +61,7 @@ function build_supervisor() {
|
||||
docker run --rm --privileged \
|
||||
-v /run/docker.sock:/run/docker.sock -v "$(pwd):/data" \
|
||||
homeassistant/amd64-builder:dev \
|
||||
--supervisor 3.7-alpine3.11 --version dev \
|
||||
-t /data --test --amd64 \
|
||||
--no-cache --docker-hub homeassistant
|
||||
--generic dev -t /data --test --amd64 --no-cache
|
||||
}
|
||||
|
||||
|
||||
@ -79,7 +77,7 @@ function cleanup_lastboot() {
|
||||
|
||||
function cleanup_docker() {
|
||||
echo "Cleaning up stopped containers..."
|
||||
docker rm $(docker ps -a -q)
|
||||
docker rm $(docker ps -a -q) || true
|
||||
}
|
||||
|
||||
|
||||
@ -108,6 +106,22 @@ function setup_test_env() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function init_dbus() {
|
||||
if pgrep dbus-daemon; then
|
||||
echo "Dbus is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Startup dbus"
|
||||
mkdir -p /var/lib/dbus
|
||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
||||
|
||||
# run
|
||||
mkdir -p /run/dbus
|
||||
dbus-daemon --system --print-address
|
||||
}
|
||||
|
||||
echo "Start Test-Env"
|
||||
|
||||
start_docker
|
||||
@ -117,5 +131,6 @@ build_supervisor
|
||||
install_cli
|
||||
cleanup_lastboot
|
||||
cleanup_docker
|
||||
init_dbus
|
||||
setup_test_env
|
||||
stop_docker
|
||||
|
1
setup.py
1
setup.py
@ -1,3 +1,4 @@
|
||||
"""Home Assistant Supervisor setup."""
|
||||
from setuptools import setup
|
||||
|
||||
from supervisor.const import SUPERVISOR_VERSION
|
||||
|
@ -152,9 +152,9 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.remove_data()
|
||||
|
||||
# Cleanup audio settings
|
||||
if addon.path_asound.exists():
|
||||
if addon.path_pulse.exists():
|
||||
with suppress(OSError):
|
||||
addon.path_asound.unlink()
|
||||
addon.path_pulse.unlink()
|
||||
|
||||
# Cleanup AppArmor profile
|
||||
with suppress(HostAppArmorError):
|
||||
|
@ -279,14 +279,14 @@ class Addon(AddonModel):
|
||||
|
||||
@property
|
||||
def audio_output(self) -> Optional[str]:
|
||||
"""Return ALSA config for output or None."""
|
||||
"""Return a pulse profile for output or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
return self.persist.get(ATTR_AUDIO_OUTPUT, self.sys_host.alsa.default.output)
|
||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||
|
||||
@audio_output.setter
|
||||
def audio_output(self, value: Optional[str]):
|
||||
"""Set/reset audio output settings."""
|
||||
"""Set/reset audio output profile settings."""
|
||||
if value is None:
|
||||
self.persist.pop(ATTR_AUDIO_OUTPUT, None)
|
||||
else:
|
||||
@ -294,10 +294,10 @@ class Addon(AddonModel):
|
||||
|
||||
@property
|
||||
def audio_input(self) -> Optional[str]:
|
||||
"""Return ALSA config for input or None."""
|
||||
"""Return pulse profile for input or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
return self.persist.get(ATTR_AUDIO_INPUT, self.sys_host.alsa.default.input)
|
||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
||||
|
||||
@audio_input.setter
|
||||
def audio_input(self, value: Optional[str]):
|
||||
@ -333,14 +333,14 @@ class Addon(AddonModel):
|
||||
return Path(self.path_data, "options.json")
|
||||
|
||||
@property
|
||||
def path_asound(self):
|
||||
def path_pulse(self):
|
||||
"""Return path to asound config."""
|
||||
return Path(self.sys_config.path_tmp, f"{self.slug}_asound")
|
||||
return Path(self.sys_config.path_tmp, f"{self.slug}_pulse")
|
||||
|
||||
@property
|
||||
def path_extern_asound(self):
|
||||
def path_extern_pulse(self):
|
||||
"""Return path to asound config for Docker."""
|
||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound")
|
||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||
|
||||
def save_persist(self):
|
||||
"""Save data of add-on."""
|
||||
@ -379,20 +379,24 @@ class Addon(AddonModel):
|
||||
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
||||
await remove_data(self.path_data)
|
||||
|
||||
def write_asound(self):
|
||||
def write_pulse(self):
|
||||
"""Write asound config to file and return True on success."""
|
||||
asound_config = self.sys_host.alsa.asound(
|
||||
alsa_input=self.audio_input, alsa_output=self.audio_output
|
||||
pulse_config = self.sys_audio.pulse_client(
|
||||
input_profile=self.audio_input, output_profile=self.audio_output
|
||||
)
|
||||
|
||||
try:
|
||||
with self.path_asound.open("w") as config_file:
|
||||
config_file.write(asound_config)
|
||||
with self.path_pulse.open("w") as config_file:
|
||||
config_file.write(pulse_config)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Add-on %s can't write asound: %s", self.slug, err)
|
||||
_LOGGER.error(
|
||||
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
||||
)
|
||||
raise AddonsError()
|
||||
|
||||
_LOGGER.debug("Add-on %s write asound: %s", self.slug, self.path_asound)
|
||||
_LOGGER.debug(
|
||||
"Add-on %s write pulse/client.config: %s", self.slug, self.path_pulse
|
||||
)
|
||||
|
||||
async def install_apparmor(self) -> None:
|
||||
"""Install or Update AppArmor profile for Add-on."""
|
||||
@ -468,7 +472,7 @@ class Addon(AddonModel):
|
||||
|
||||
# Sound
|
||||
if self.with_audio:
|
||||
self.write_asound()
|
||||
self.write_pulse()
|
||||
|
||||
# Start Add-on
|
||||
try:
|
||||
|
@ -96,7 +96,6 @@ from ..discovery.validate import valid_discovery_service
|
||||
from ..validate import (
|
||||
DOCKER_PORTS,
|
||||
DOCKER_PORTS_DESCRIPTION,
|
||||
alsa_device,
|
||||
network_port,
|
||||
token,
|
||||
uuid_match,
|
||||
@ -296,8 +295,8 @@ SCHEMA_ADDON_USER = vol.Schema(
|
||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): alsa_device,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): alsa_device,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ from .security import SecurityMiddleware
|
||||
from .services import APIServices
|
||||
from .snapshots import APISnapshots
|
||||
from .supervisor import APISupervisor
|
||||
from .audio import APIAudio
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -314,6 +315,21 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_audio(self) -> None:
|
||||
"""Register Audio functions."""
|
||||
api_audio = APIAudio()
|
||||
api_audio.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/audio/info", api_audio.info),
|
||||
web.get("/audio/stats", api_audio.stats),
|
||||
web.get("/audio/logs", api_audio.logs),
|
||||
web.post("/audio/update", api_audio.update),
|
||||
web.post("/audio/restart", api_audio.restart),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_panel(self) -> None:
|
||||
"""Register panel for Home Assistant."""
|
||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||
|
@ -96,7 +96,7 @@ from ..const import (
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import APIError
|
||||
from ..validate import DOCKER_PORTS, alsa_device
|
||||
from ..validate import DOCKER_PORTS
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -107,10 +107,10 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||
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_NETWORK): vol.Maybe(DOCKER_PORTS),
|
||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): alsa_device,
|
||||
vol.Optional(ATTR_AUDIO_INPUT): alsa_device,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
78
supervisor/api/audio.py
Normal file
78
supervisor/api/audio.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Init file for Supervisor Audio RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import (
|
||||
ATTR_BLK_READ,
|
||||
ATTR_BLK_WRITE,
|
||||
ATTR_CPU_PERCENT,
|
||||
ATTR_HOST,
|
||||
ATTR_LATEST_VERSION,
|
||||
ATTR_MEMORY_LIMIT,
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_MEMORY_USAGE,
|
||||
ATTR_NETWORK_RX,
|
||||
ATTR_NETWORK_TX,
|
||||
ATTR_VERSION,
|
||||
CONTENT_TYPE_BINARY,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||
|
||||
|
||||
class APIAudio(CoreSysAttributes):
|
||||
"""Handle RESTful API for Audio functions."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return Audio information."""
|
||||
return {
|
||||
ATTR_VERSION: self.sys_audio.version,
|
||||
ATTR_LATEST_VERSION: self.sys_audio.latest_version,
|
||||
ATTR_HOST: str(self.sys_docker.network.audio),
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return resource information."""
|
||||
stats = await self.sys_audio.stats()
|
||||
|
||||
return {
|
||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||
ATTR_NETWORK_RX: stats.network_rx,
|
||||
ATTR_NETWORK_TX: stats.network_tx,
|
||||
ATTR_BLK_READ: stats.blk_read,
|
||||
ATTR_BLK_WRITE: stats.blk_write,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def update(self, request: web.Request) -> None:
|
||||
"""Update Audio plugin."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.sys_audio.latest_version)
|
||||
|
||||
if version == self.sys_audio.version:
|
||||
raise APIError("Version {} is already in use".format(version))
|
||||
await asyncio.shield(self.sys_audio.update(version))
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||
"""Return Audio Docker logs."""
|
||||
return self.sys_audio.logs()
|
||||
|
||||
@api_process
|
||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Restart Audio plugin."""
|
||||
return asyncio.shield(self.sys_audio.restart())
|
@ -37,13 +37,8 @@ class APIHardware(CoreSysAttributes):
|
||||
|
||||
@api_process
|
||||
async def audio(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Show ALSA audio devices."""
|
||||
return {
|
||||
ATTR_AUDIO: {
|
||||
ATTR_INPUT: self.sys_host.alsa.input_devices,
|
||||
ATTR_OUTPUT: self.sys_host.alsa.output_devices,
|
||||
}
|
||||
}
|
||||
"""Show pulse audio profiles."""
|
||||
return {ATTR_AUDIO: {ATTR_INPUT: [], ATTR_OUTPUT: []}}
|
||||
|
||||
@api_process
|
||||
def trigger(self, request: web.Request) -> None:
|
||||
|
199
supervisor/audio.py
Normal file
199
supervisor/audio.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""Home Assistant control object."""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
from typing import Awaitable, Optional
|
||||
|
||||
from .const import ATTR_VERSION, FILE_HASSIO_AUDIO
|
||||
from .coresys import CoreSys, CoreSysAttributes
|
||||
from .docker.audio import DockerAudio
|
||||
from .docker.stats import DockerStats
|
||||
from .exceptions import AudioError, AudioUpdateError, DockerAPIError
|
||||
from .utils.json import JsonConfig
|
||||
from .validate import SCHEMA_AUDIO_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PULSE_CLIENT_TMPL: Path = Path(__file__).parents[0].joinpath("data/pulse-client.tmpl")
|
||||
|
||||
|
||||
class Audio(JsonConfig, CoreSysAttributes):
|
||||
"""Home Assistant core object for handle audio."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
"""Initialize hass object."""
|
||||
super().__init__(FILE_HASSIO_AUDIO, SCHEMA_AUDIO_CONFIG)
|
||||
self.coresys: CoreSys = coresys
|
||||
self.instance: DockerAudio = DockerAudio(coresys)
|
||||
|
||||
@property
|
||||
def path_extern_data(self) -> Path:
|
||||
"""Return path of pulse cookie file."""
|
||||
return self.sys_config.path_extern_audio.joinpath("external")
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
"""Return current version of Audio."""
|
||||
return self._data.get(ATTR_VERSION)
|
||||
|
||||
@version.setter
|
||||
def version(self, value: str) -> None:
|
||||
"""Return current version of Audio."""
|
||||
self._data[ATTR_VERSION] = value
|
||||
|
||||
@property
|
||||
def latest_version(self) -> Optional[str]:
|
||||
"""Return latest version of Audio."""
|
||||
return self.sys_updater.version_audio
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Return True if a task is in progress."""
|
||||
return self.instance.in_progress
|
||||
|
||||
@property
|
||||
def need_update(self) -> bool:
|
||||
"""Return True if an update is available."""
|
||||
return self.version != self.latest_version
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load Audio setup."""
|
||||
|
||||
# Check Audio state
|
||||
try:
|
||||
# Evaluate Version if we lost this information
|
||||
if not self.version:
|
||||
self.version = await self.instance.get_latest_version(key=int)
|
||||
|
||||
await self.instance.attach(tag=self.version)
|
||||
except DockerAPIError:
|
||||
_LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image)
|
||||
|
||||
# Install CoreDNS
|
||||
with suppress(AudioError):
|
||||
await self.install()
|
||||
else:
|
||||
self.version = self.instance.version
|
||||
self.save_data()
|
||||
|
||||
# Run CoreDNS
|
||||
with suppress(AudioError):
|
||||
if await self.instance.is_running():
|
||||
await self.restart()
|
||||
else:
|
||||
await self.start()
|
||||
|
||||
async def install(self) -> None:
|
||||
"""Install Audio."""
|
||||
_LOGGER.info("Setup Audio plugin")
|
||||
while True:
|
||||
# read audio tag and install it
|
||||
if not self.latest_version:
|
||||
await self.sys_updater.reload()
|
||||
|
||||
if self.latest_version:
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.install(self.latest_version)
|
||||
break
|
||||
_LOGGER.warning("Error on install Audio plugin. Retry in 30sec")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
_LOGGER.info("Audio plugin now installed")
|
||||
self.version = self.instance.version
|
||||
self.save_data()
|
||||
|
||||
async def update(self, version: Optional[str] = None) -> None:
|
||||
"""Update Audio plugin."""
|
||||
version = version or self.latest_version
|
||||
|
||||
if version == self.version:
|
||||
_LOGGER.warning("Version %s is already installed for Audio", version)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.instance.update(version)
|
||||
except DockerAPIError:
|
||||
_LOGGER.error("Audio update fails")
|
||||
raise AudioUpdateError() from None
|
||||
else:
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.cleanup()
|
||||
|
||||
self.version = version
|
||||
self.save_data()
|
||||
|
||||
# Start Audio
|
||||
await self.start()
|
||||
|
||||
async def restart(self) -> None:
|
||||
"""Restart Audio plugin."""
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.restart()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Run CoreDNS."""
|
||||
# Start Instance
|
||||
_LOGGER.info("Start Audio plugin")
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerAPIError:
|
||||
_LOGGER.error("Can't start Audio plugin")
|
||||
raise AudioError() from None
|
||||
|
||||
def logs(self) -> Awaitable[bytes]:
|
||||
"""Get CoreDNS docker logs.
|
||||
|
||||
Return Coroutine.
|
||||
"""
|
||||
return self.instance.logs()
|
||||
|
||||
async def stats(self) -> DockerStats:
|
||||
"""Return stats of CoreDNS."""
|
||||
try:
|
||||
return await self.instance.stats()
|
||||
except DockerAPIError:
|
||||
raise AudioError() from None
|
||||
|
||||
def is_running(self) -> Awaitable[bool]:
|
||||
"""Return True if Docker container is running.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.is_running()
|
||||
|
||||
def is_fails(self) -> Awaitable[bool]:
|
||||
"""Return True if a Docker container is fails state.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.is_fails()
|
||||
|
||||
async def repair(self) -> None:
|
||||
"""Repair CoreDNS plugin."""
|
||||
if await self.instance.exists():
|
||||
return
|
||||
|
||||
_LOGGER.info("Repair Audio %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version)
|
||||
except DockerAPIError:
|
||||
_LOGGER.error("Repairing of Audio fails")
|
||||
|
||||
def pulse_client(self, input_profile=None, output_profile=None) -> str:
|
||||
"""Generate an /etc/pulse/client.conf data."""
|
||||
|
||||
# Read Template
|
||||
try:
|
||||
config_data = PULSE_CLIENT_TMPL.read_text()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't read pulse-client.tmpl: %s", err)
|
||||
return ""
|
||||
|
||||
# Process Template
|
||||
config_template = Template(config_data)
|
||||
return config_template.safe_substitute(
|
||||
audio_address=self.sys_docker.network.audio
|
||||
)
|
@ -11,6 +11,7 @@ from .addons import AddonManager
|
||||
from .api import RestAPI
|
||||
from .arch import CpuArch
|
||||
from .auth import Auth
|
||||
from .audio import Audio
|
||||
from .const import SOCKET_DOCKER, UpdateChannels
|
||||
from .core import Core
|
||||
from .coresys import CoreSys
|
||||
@ -47,6 +48,7 @@ async def initialize_coresys():
|
||||
coresys.core = Core(coresys)
|
||||
coresys.dns = CoreDNS(coresys)
|
||||
coresys.arch = CpuArch(coresys)
|
||||
coresys.audio = Audio(coresys)
|
||||
coresys.auth = Auth(coresys)
|
||||
coresys.updater = Updater(coresys)
|
||||
coresys.api = RestAPI(coresys)
|
||||
@ -89,12 +91,12 @@ def initialize_system_data(coresys: CoreSys):
|
||||
)
|
||||
config.path_homeassistant.mkdir()
|
||||
|
||||
# supervisor ssl folder
|
||||
# Supervisor ssl folder
|
||||
if not config.path_ssl.is_dir():
|
||||
_LOGGER.info("Create Supervisor SSL/TLS folder %s", config.path_ssl)
|
||||
config.path_ssl.mkdir()
|
||||
|
||||
# supervisor addon data folder
|
||||
# Supervisor addon data folder
|
||||
if not config.path_addons_data.is_dir():
|
||||
_LOGGER.info("Create Supervisor Add-on data folder %s", config.path_addons_data)
|
||||
config.path_addons_data.mkdir(parents=True)
|
||||
@ -113,31 +115,36 @@ def initialize_system_data(coresys: CoreSys):
|
||||
)
|
||||
config.path_addons_git.mkdir(parents=True)
|
||||
|
||||
# supervisor tmp folder
|
||||
# Supervisor tmp folder
|
||||
if not config.path_tmp.is_dir():
|
||||
_LOGGER.info("Create Supervisor temp folder %s", config.path_tmp)
|
||||
config.path_tmp.mkdir(parents=True)
|
||||
|
||||
# supervisor backup folder
|
||||
# Supervisor backup folder
|
||||
if not config.path_backup.is_dir():
|
||||
_LOGGER.info("Create Supervisor backup folder %s", config.path_backup)
|
||||
config.path_backup.mkdir()
|
||||
|
||||
# share folder
|
||||
# Share folder
|
||||
if not config.path_share.is_dir():
|
||||
_LOGGER.info("Create Supervisor share folder %s", config.path_share)
|
||||
config.path_share.mkdir()
|
||||
|
||||
# apparmor folder
|
||||
# Apparmor folder
|
||||
if not config.path_apparmor.is_dir():
|
||||
_LOGGER.info("Create Supervisor Apparmor folder %s", config.path_apparmor)
|
||||
config.path_apparmor.mkdir()
|
||||
|
||||
# dns folder
|
||||
# DNS folder
|
||||
if not config.path_dns.is_dir():
|
||||
_LOGGER.info("Create Supervisor DNS folder %s", config.path_dns)
|
||||
config.path_dns.mkdir()
|
||||
|
||||
# Audio folder
|
||||
if not config.path_audio.is_dir():
|
||||
_LOGGER.info("Create Supervisor audio folder %s", config.path_audio)
|
||||
config.path_audio.mkdir()
|
||||
|
||||
# Update log level
|
||||
coresys.config.modify_log_level()
|
||||
|
||||
|
@ -13,7 +13,7 @@ from .const import (
|
||||
ATTR_TIMEZONE,
|
||||
ATTR_WAIT_BOOT,
|
||||
FILE_HASSIO_CONFIG,
|
||||
HASSIO_DATA,
|
||||
SUPERVISOR_DATA,
|
||||
)
|
||||
from .utils.dt import parse_datetime
|
||||
from .utils.json import JsonConfig
|
||||
@ -35,6 +35,7 @@ SHARE_DATA = PurePath("share")
|
||||
TMP_DATA = PurePath("tmp")
|
||||
APPARMOR_DATA = PurePath("apparmor")
|
||||
DNS_DATA = PurePath("dns")
|
||||
AUDIO_DATA = PurePath("audio")
|
||||
|
||||
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
|
||||
|
||||
@ -120,7 +121,7 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_hassio(self):
|
||||
"""Return Supervisor data path."""
|
||||
return HASSIO_DATA
|
||||
return SUPERVISOR_DATA
|
||||
|
||||
@property
|
||||
def path_extern_hassio(self):
|
||||
@ -135,7 +136,7 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_homeassistant(self):
|
||||
"""Return config path inside supervisor."""
|
||||
return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG)
|
||||
return Path(SUPERVISOR_DATA, HOMEASSISTANT_CONFIG)
|
||||
|
||||
@property
|
||||
def path_extern_ssl(self):
|
||||
@ -145,22 +146,22 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_ssl(self):
|
||||
"""Return SSL path inside supervisor."""
|
||||
return Path(HASSIO_DATA, HASSIO_SSL)
|
||||
return Path(SUPERVISOR_DATA, HASSIO_SSL)
|
||||
|
||||
@property
|
||||
def path_addons_core(self):
|
||||
"""Return git path for core Add-ons."""
|
||||
return Path(HASSIO_DATA, ADDONS_CORE)
|
||||
return Path(SUPERVISOR_DATA, ADDONS_CORE)
|
||||
|
||||
@property
|
||||
def path_addons_git(self):
|
||||
"""Return path for Git Add-on."""
|
||||
return Path(HASSIO_DATA, ADDONS_GIT)
|
||||
return Path(SUPERVISOR_DATA, ADDONS_GIT)
|
||||
|
||||
@property
|
||||
def path_addons_local(self):
|
||||
"""Return path for custom Add-ons."""
|
||||
return Path(HASSIO_DATA, ADDONS_LOCAL)
|
||||
return Path(SUPERVISOR_DATA, ADDONS_LOCAL)
|
||||
|
||||
@property
|
||||
def path_extern_addons_local(self):
|
||||
@ -170,17 +171,27 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_addons_data(self):
|
||||
"""Return root Add-on data folder."""
|
||||
return Path(HASSIO_DATA, ADDONS_DATA)
|
||||
return Path(SUPERVISOR_DATA, ADDONS_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_addons_data(self):
|
||||
"""Return root add-on data folder external for Docker."""
|
||||
return PurePath(self.path_extern_hassio, ADDONS_DATA)
|
||||
|
||||
@property
|
||||
def path_audio(self):
|
||||
"""Return root audio data folder."""
|
||||
return Path(SUPERVISOR_DATA, AUDIO_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_audio(self):
|
||||
"""Return root audio data folder external for Docker."""
|
||||
return PurePath(self.path_extern_hassio, AUDIO_DATA)
|
||||
|
||||
@property
|
||||
def path_tmp(self):
|
||||
"""Return Supervisor temp folder."""
|
||||
return Path(HASSIO_DATA, TMP_DATA)
|
||||
return Path(SUPERVISOR_DATA, TMP_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_tmp(self):
|
||||
@ -190,7 +201,7 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_backup(self):
|
||||
"""Return root backup data folder."""
|
||||
return Path(HASSIO_DATA, BACKUP_DATA)
|
||||
return Path(SUPERVISOR_DATA, BACKUP_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_backup(self):
|
||||
@ -200,12 +211,12 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_share(self):
|
||||
"""Return root share data folder."""
|
||||
return Path(HASSIO_DATA, SHARE_DATA)
|
||||
return Path(SUPERVISOR_DATA, SHARE_DATA)
|
||||
|
||||
@property
|
||||
def path_apparmor(self):
|
||||
"""Return root Apparmor profile folder."""
|
||||
return Path(HASSIO_DATA, APPARMOR_DATA)
|
||||
return Path(SUPERVISOR_DATA, APPARMOR_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_share(self):
|
||||
@ -220,7 +231,7 @@ class CoreConfig(JsonConfig):
|
||||
@property
|
||||
def path_dns(self):
|
||||
"""Return dns path inside supervisor."""
|
||||
return Path(HASSIO_DATA, DNS_DATA)
|
||||
return Path(SUPERVISOR_DATA, DNS_DATA)
|
||||
|
||||
@property
|
||||
def addons_repositories(self):
|
||||
|
@ -15,17 +15,18 @@ URL_HASSOS_OTA = (
|
||||
"{version}/hassos_{board}-{version}.raucb"
|
||||
)
|
||||
|
||||
HASSIO_DATA = Path("/data")
|
||||
SUPERVISOR_DATA = Path("/data")
|
||||
|
||||
FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json")
|
||||
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
||||
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
||||
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
||||
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
|
||||
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
|
||||
FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json")
|
||||
FILE_HASSIO_INGRESS = Path(HASSIO_DATA, "ingress.json")
|
||||
FILE_HASSIO_DNS = Path(HASSIO_DATA, "dns.json")
|
||||
FILE_HASSIO_AUTH = Path(SUPERVISOR_DATA, "auth.json")
|
||||
FILE_HASSIO_ADDONS = Path(SUPERVISOR_DATA, "addons.json")
|
||||
FILE_HASSIO_CONFIG = Path(SUPERVISOR_DATA, "config.json")
|
||||
FILE_HASSIO_HOMEASSISTANT = Path(SUPERVISOR_DATA, "homeassistant.json")
|
||||
FILE_HASSIO_UPDATER = Path(SUPERVISOR_DATA, "updater.json")
|
||||
FILE_HASSIO_SERVICES = Path(SUPERVISOR_DATA, "services.json")
|
||||
FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json")
|
||||
FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json")
|
||||
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
||||
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
||||
|
||||
SOCKET_DOCKER = Path("/var/run/docker.sock")
|
||||
|
||||
@ -229,6 +230,7 @@ ATTR_SNAPSHOT_EXCLUDE = "snapshot_exclude"
|
||||
ATTR_DOCUMENTATION = "documentation"
|
||||
ATTR_ADVANCED = "advanced"
|
||||
ATTR_STAGE = "stage"
|
||||
ATTR_CLI = "cli"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
|
@ -40,8 +40,8 @@ class Core(CoreSysAttributes):
|
||||
# Load Host
|
||||
await self.sys_host.load()
|
||||
|
||||
# Load CoreDNS
|
||||
await self.sys_dns.load()
|
||||
# Load Plugins container
|
||||
await asyncio.wait([self.sys_dns.load(), self.sys_audio.load()])
|
||||
|
||||
# Load Home Assistant
|
||||
await self.sys_homeassistant.load()
|
||||
|
@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
||||
from .addons import AddonManager
|
||||
from .api import RestAPI
|
||||
from .arch import CpuArch
|
||||
from .audio import Audio
|
||||
from .auth import Auth
|
||||
from .core import Core
|
||||
from .dbus import DBusManager
|
||||
@ -57,6 +58,7 @@ class CoreSys:
|
||||
# Internal objects pointers
|
||||
self._core: Optional[Core] = None
|
||||
self._arch: Optional[CpuArch] = None
|
||||
self._audio: Optional[Audio] = None
|
||||
self._auth: Optional[Auth] = None
|
||||
self._dns: Optional[CoreDNS] = None
|
||||
self._homeassistant: Optional[HomeAssistant] = None
|
||||
@ -163,6 +165,18 @@ class CoreSys:
|
||||
raise RuntimeError("Auth already set!")
|
||||
self._auth = value
|
||||
|
||||
@property
|
||||
def audio(self) -> Audio:
|
||||
"""Return Audio object."""
|
||||
return self._audio
|
||||
|
||||
@audio.setter
|
||||
def audio(self, value: Audio):
|
||||
"""Set a Audio object."""
|
||||
if self._audio:
|
||||
raise RuntimeError("Audio already set!")
|
||||
self._audio = value
|
||||
|
||||
@property
|
||||
def homeassistant(self) -> HomeAssistant:
|
||||
"""Return Home Assistant object."""
|
||||
@ -431,6 +445,11 @@ class CoreSysAttributes:
|
||||
"""Return Auth object."""
|
||||
return self.coresys.auth
|
||||
|
||||
@property
|
||||
def sys_audio(self) -> Audio:
|
||||
"""Return Audio object."""
|
||||
return self.coresys.audio
|
||||
|
||||
@property
|
||||
def sys_homeassistant(self) -> HomeAssistant:
|
||||
"""Return Home Assistant object."""
|
||||
|
@ -1,17 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
35
supervisor/data/pulse-client.tmpl
Normal file
35
supervisor/data/pulse-client.tmpl
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://run/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
|
@ -35,7 +35,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
||||
NO_ADDDRESS = ip_address("0.0.0.0")
|
||||
|
||||
|
||||
@ -131,10 +130,6 @@ class DockerAddon(DockerInterface):
|
||||
if self.addon.devices:
|
||||
devices.extend(self.addon.devices)
|
||||
|
||||
# Use audio devices
|
||||
if self.addon.with_audio and self.sys_hardware.support_audio:
|
||||
devices.append(AUDIO_DEVICE)
|
||||
|
||||
# Auto mapping UART devices
|
||||
if self.addon.auto_uart:
|
||||
if self.addon.with_udev:
|
||||
@ -298,21 +293,25 @@ class DockerAddon(DockerInterface):
|
||||
# Docker API support
|
||||
if not self.addon.protected and self.addon.access_docker_api:
|
||||
volumes.update(
|
||||
{"/var/run/docker.sock": {"bind": "/var/run/docker.sock", "mode": "ro"}}
|
||||
{"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}}
|
||||
)
|
||||
|
||||
# Host D-Bus system
|
||||
if self.addon.host_dbus:
|
||||
volumes.update({"/var/run/dbus": {"bind": "/var/run/dbus", "mode": "rw"}})
|
||||
volumes.update({"/run/dbus": {"bind": "/run/dbus", "mode": "rw"}})
|
||||
|
||||
# ALSA configuration
|
||||
# Configuration Audio
|
||||
if self.addon.with_audio:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.addon.path_extern_asound): {
|
||||
"bind": "/etc/asound.conf",
|
||||
str(self.addon.path_extern_pulse): {
|
||||
"bind": "/etc/pulse/client.conf",
|
||||
"mode": "ro",
|
||||
}
|
||||
},
|
||||
str(self.sys_audio.path_extern_data.joinpath("pulse.sock")): {
|
||||
"bind": "/run/pulse.sock",
|
||||
"mode": "rw",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
66
supervisor/docker/audio.py
Normal file
66
supervisor/docker/audio.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Audio docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from ..const import ENV_TIME
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
AUDIO_DOCKER_NAME: str = "hassio_audio"
|
||||
|
||||
|
||||
class DockerAudio(DockerInterface, CoreSysAttributes):
|
||||
"""Docker Supervisor wrapper for Supervisor Audio."""
|
||||
|
||||
@property
|
||||
def image(self) -> str:
|
||||
"""Return name of Supervisor Audio image."""
|
||||
return f"homeassistant/{self.sys_arch.supervisor}-hassio-audio"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of Docker container."""
|
||||
return AUDIO_DOCKER_NAME
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Run Docker image.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
if self._is_running():
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
self.image,
|
||||
version=self.sys_audio.version,
|
||||
ipv4=self.sys_docker.network.audio,
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
privileged=True,
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
volumes={
|
||||
str(self.sys_config.path_extern_audio): {
|
||||
"bind": "/data",
|
||||
"mode": "rw",
|
||||
},
|
||||
"/dev/snd": {"bind": "/dev/snd", "mode": "rw"},
|
||||
"/etc/group": {"bind": "/host/group", "mode": "ro"},
|
||||
},
|
||||
)
|
||||
|
||||
self._meta = docker_container.attrs
|
||||
_LOGGER.info(
|
||||
"Start Audio %s with version %s - %s",
|
||||
self.image,
|
||||
self.version,
|
||||
self.sys_docker.network.audio,
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
"""HassOS Cli docker object."""
|
||||
"""DNS docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
@ -46,7 +46,6 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
init=True,
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
volumes={
|
||||
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"}
|
||||
|
@ -48,6 +48,11 @@ class DockerNetwork:
|
||||
"""Return dns of the network."""
|
||||
return DOCKER_NETWORK_MASK[3]
|
||||
|
||||
@property
|
||||
def audio(self) -> IPv4Address:
|
||||
"""Return audio of the network."""
|
||||
return DOCKER_NETWORK_MASK[4]
|
||||
|
||||
def _get_network(self) -> docker.models.networks.Network:
|
||||
"""Get supervisor network."""
|
||||
try:
|
||||
|
@ -65,6 +65,17 @@ class CoreDNSUpdateError(CoreDNSError):
|
||||
"""Error on update of a CoreDNS."""
|
||||
|
||||
|
||||
# DNS
|
||||
|
||||
|
||||
class AudioError(HassioError):
|
||||
"""PulseAudio exception."""
|
||||
|
||||
|
||||
class AudioUpdateError(AudioError):
|
||||
"""Error on update of a Audio."""
|
||||
|
||||
|
||||
# Addons
|
||||
|
||||
|
||||
|
@ -56,7 +56,7 @@ class HassOS(CoreSysAttributes):
|
||||
@property
|
||||
def version_cli_latest(self) -> str:
|
||||
"""Return version of HassOS."""
|
||||
return self.sys_updater.version_hassos_cli
|
||||
return self.sys_updater.version_cli
|
||||
|
||||
@property
|
||||
def need_update(self) -> bool:
|
||||
|
@ -2,7 +2,6 @@
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from .alsa import AlsaAudio
|
||||
from .apparmor import AppArmorControl
|
||||
from .control import SystemControl
|
||||
from .info import InfoCenter
|
||||
@ -28,18 +27,12 @@ class HostManager(CoreSysAttributes):
|
||||
"""Initialize Host manager."""
|
||||
self.coresys: CoreSys = coresys
|
||||
|
||||
self._alsa: AlsaAudio = AlsaAudio(coresys)
|
||||
self._apparmor: AppArmorControl = AppArmorControl(coresys)
|
||||
self._control: SystemControl = SystemControl(coresys)
|
||||
self._info: InfoCenter = InfoCenter(coresys)
|
||||
self._services: ServiceManager = ServiceManager(coresys)
|
||||
self._network: NetworkManager = NetworkManager(coresys)
|
||||
|
||||
@property
|
||||
def alsa(self) -> AlsaAudio:
|
||||
"""Return host ALSA handler."""
|
||||
return self._alsa
|
||||
|
||||
@property
|
||||
def apparmor(self) -> AppArmorControl:
|
||||
"""Return host AppArmor handler."""
|
||||
|
@ -1,138 +0,0 @@
|
||||
"""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, CHAN_ID, CHAN_TYPE
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attr.s()
|
||||
class DefaultConfig:
|
||||
"""Default config input/output ALSA channel."""
|
||||
|
||||
input: str = attr.ib()
|
||||
output: str = attr.ib()
|
||||
|
||||
|
||||
AUDIODB_JSON: Path = Path(__file__).parents[1].joinpath("data/audiodb.json")
|
||||
ASOUND_TMPL: Path = Path(__file__).parents[1].joinpath("data/asound.tmpl")
|
||||
|
||||
|
||||
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.sys_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.sys_hardware.audio_devices.items():
|
||||
for chan_info in dev_data[ATTR_DEVICES]:
|
||||
chan_id = chan_info[CHAN_ID]
|
||||
chan_type = chan_info[CHAN_TYPE]
|
||||
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.sys_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."""
|
||||
try:
|
||||
return json.loads(AUDIODB_JSON.read_text())
|
||||
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.sys_machine, {}).get(ATTR_INPUT)
|
||||
alsa_output = database.get(self.sys_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 an asound data."""
|
||||
alsa_input = alsa_input or self.default.input
|
||||
alsa_output = alsa_output or self.default.output
|
||||
|
||||
# Read Template
|
||||
try:
|
||||
asound_data = ASOUND_TMPL.read_text()
|
||||
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)
|
@ -199,7 +199,9 @@ class Hardware:
|
||||
|
||||
async def udev_trigger(self) -> None:
|
||||
"""Trigger a udev reload."""
|
||||
proc = await asyncio.create_subprocess_exec("udevadm", "trigger")
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
"udevadm trigger && udevadm settle"
|
||||
)
|
||||
|
||||
await proc.wait()
|
||||
if proc.returncode == 0:
|
||||
|
@ -11,8 +11,9 @@ HASS_WATCHDOG_API = "HASS_WATCHDOG_API"
|
||||
|
||||
RUN_UPDATE_SUPERVISOR = 29100
|
||||
RUN_UPDATE_ADDONS = 57600
|
||||
RUN_UPDATE_HASSOSCLI = 28100
|
||||
RUN_UPDATE_CLI = 28100
|
||||
RUN_UPDATE_DNS = 30100
|
||||
RUN_UPDATE_AUDIO = 30200
|
||||
|
||||
RUN_RELOAD_ADDONS = 10800
|
||||
RUN_RELOAD_SNAPSHOTS = 72000
|
||||
@ -24,6 +25,7 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
|
||||
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
||||
|
||||
RUN_WATCHDOG_DNS_DOCKER = 20
|
||||
RUN_WATCHDOG_AUDIO_DOCKER = 20
|
||||
|
||||
|
||||
class Tasks(CoreSysAttributes):
|
||||
@ -47,13 +49,14 @@ class Tasks(CoreSysAttributes):
|
||||
)
|
||||
)
|
||||
self.jobs.add(
|
||||
self.sys_scheduler.register_task(
|
||||
self._update_hassos_cli, RUN_UPDATE_HASSOSCLI
|
||||
)
|
||||
self.sys_scheduler.register_task(self._update_cli, RUN_UPDATE_CLI)
|
||||
)
|
||||
self.jobs.add(
|
||||
self.sys_scheduler.register_task(self._update_dns, RUN_UPDATE_DNS)
|
||||
)
|
||||
self.jobs.add(
|
||||
self.sys_scheduler.register_task(self._update_audio, RUN_UPDATE_AUDIO)
|
||||
)
|
||||
|
||||
# Reload
|
||||
self.jobs.add(
|
||||
@ -94,6 +97,11 @@ class Tasks(CoreSysAttributes):
|
||||
self._watchdog_dns_docker, RUN_WATCHDOG_DNS_DOCKER
|
||||
)
|
||||
)
|
||||
self.jobs.add(
|
||||
self.sys_scheduler.register_task(
|
||||
self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER
|
||||
)
|
||||
)
|
||||
|
||||
_LOGGER.info("All core tasks are scheduled")
|
||||
|
||||
@ -193,17 +201,12 @@ class Tasks(CoreSysAttributes):
|
||||
finally:
|
||||
self._cache[HASS_WATCHDOG_API] = 0
|
||||
|
||||
async def _update_hassos_cli(self):
|
||||
"""Check and run update of HassOS CLI."""
|
||||
async def _update_cli(self):
|
||||
"""Check and run update of CLI."""
|
||||
if not self.sys_hassos.need_cli_update:
|
||||
return
|
||||
|
||||
# don't perform an update on dev channel
|
||||
if self.sys_dev:
|
||||
_LOGGER.warning("Ignore HassOS CLI update on dev channel!")
|
||||
return
|
||||
|
||||
_LOGGER.info("Found new HassOS CLI version")
|
||||
_LOGGER.info("Found new CLI version")
|
||||
await self.sys_hassos.update_cli()
|
||||
|
||||
async def _update_dns(self):
|
||||
@ -211,17 +214,20 @@ class Tasks(CoreSysAttributes):
|
||||
if not self.sys_dns.need_update:
|
||||
return
|
||||
|
||||
# don't perform an update on dev channel
|
||||
if self.sys_dev:
|
||||
_LOGGER.warning("Ignore CoreDNS update on dev channel!")
|
||||
return
|
||||
|
||||
_LOGGER.info("Found new CoreDNS plugin version")
|
||||
await self.sys_dns.update()
|
||||
|
||||
async def _update_audio(self):
|
||||
"""Check and run update of PulseAudio plugin."""
|
||||
if not self.sys_audio.need_update:
|
||||
return
|
||||
|
||||
_LOGGER.info("Found new PulseAudio plugin version")
|
||||
await self.sys_audio.update()
|
||||
|
||||
async def _watchdog_dns_docker(self):
|
||||
"""Check running state of Docker and start if they is close."""
|
||||
# if Home Assistant is active
|
||||
# if CoreDNS is active
|
||||
if await self.sys_dns.is_running():
|
||||
return
|
||||
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
|
||||
@ -234,3 +240,15 @@ class Tasks(CoreSysAttributes):
|
||||
await self.sys_dns.start()
|
||||
except CoreDNSError:
|
||||
_LOGGER.error("Watchdog CoreDNS reanimation fails!")
|
||||
|
||||
async def _watchdog_audio_docker(self):
|
||||
"""Check running state of Docker and start if they is close."""
|
||||
# if PulseAudio plugin is active
|
||||
if await self.sys_audio.is_running():
|
||||
return
|
||||
_LOGGER.warning("Watchdog found a problem with PulseAudio plugin!")
|
||||
|
||||
try:
|
||||
await self.sys_audio.start()
|
||||
except CoreDNSError:
|
||||
_LOGGER.error("Watchdog PulseAudio reanimation fails!")
|
||||
|
@ -9,11 +9,12 @@ from typing import Optional
|
||||
import aiohttp
|
||||
|
||||
from .const import (
|
||||
ATTR_AUDIO,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_CLI,
|
||||
ATTR_DNS,
|
||||
ATTR_HASSIO,
|
||||
ATTR_HASSOS,
|
||||
ATTR_HASSOS_CLI,
|
||||
ATTR_HOMEASSISTANT,
|
||||
FILE_HASSIO_UPDATER,
|
||||
URL_HASSIO_VERSION,
|
||||
@ -62,15 +63,20 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
return self._data.get(ATTR_HASSOS)
|
||||
|
||||
@property
|
||||
def version_hassos_cli(self) -> Optional[str]:
|
||||
"""Return latest version of HassOS cli."""
|
||||
return self._data.get(ATTR_HASSOS_CLI)
|
||||
def version_cli(self) -> Optional[str]:
|
||||
"""Return latest version of CLI."""
|
||||
return self._data.get(ATTR_CLI)
|
||||
|
||||
@property
|
||||
def version_dns(self) -> Optional[str]:
|
||||
"""Return latest version of Supervisor DNS."""
|
||||
"""Return latest version of DNS."""
|
||||
return self._data.get(ATTR_DNS)
|
||||
|
||||
@property
|
||||
def version_audio(self) -> Optional[str]:
|
||||
"""Return latest version of Audio."""
|
||||
return self._data.get(ATTR_AUDIO)
|
||||
|
||||
@property
|
||||
def channel(self) -> UpdateChannels:
|
||||
"""Return upstream channel of Supervisor instance."""
|
||||
@ -81,7 +87,7 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
"""Set upstream mode."""
|
||||
self._data[ATTR_CHANNEL] = value
|
||||
|
||||
@AsyncThrottle(timedelta(seconds=60))
|
||||
@AsyncThrottle(timedelta(seconds=30))
|
||||
async def fetch_data(self):
|
||||
"""Fetch current versions from Github.
|
||||
|
||||
@ -110,17 +116,20 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
raise HassioUpdaterError() from None
|
||||
|
||||
try:
|
||||
# update supervisor version
|
||||
# Update supervisor version
|
||||
self._data[ATTR_HASSIO] = data["supervisor"]
|
||||
self._data[ATTR_DNS] = data["dns"]
|
||||
|
||||
# update Home Assistant version
|
||||
# Update Home Assistant core version
|
||||
self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine]
|
||||
|
||||
# update hassos version
|
||||
# Update HassOS version
|
||||
if self.sys_hassos.available and board:
|
||||
self._data[ATTR_HASSOS] = data["hassos"][board]
|
||||
self._data[ATTR_HASSOS_CLI] = data["hassos-cli"]
|
||||
|
||||
# Update Home Assistant services
|
||||
self._data[ATTR_CLI] = data["cli"]
|
||||
self._data[ATTR_DNS] = data["dns"]
|
||||
self._data[ATTR_AUDIO] = data["audio"]
|
||||
|
||||
except KeyError as err:
|
||||
_LOGGER.warning("Can't process version data: %s", err)
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Tools file for Supervisor."""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
|
||||
@ -41,18 +43,23 @@ class AsyncThrottle:
|
||||
"""Initialize async throttle."""
|
||||
self.throttle_period = delta
|
||||
self.time_of_last_call = datetime.min
|
||||
self.synchronize: Optional[asyncio.Lock] = None
|
||||
|
||||
def __call__(self, method):
|
||||
"""Throttle function"""
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
"""Throttle function wrapper"""
|
||||
now = datetime.now()
|
||||
time_since_last_call = now - self.time_of_last_call
|
||||
if not self.synchronize:
|
||||
self.synchronize = asyncio.Lock()
|
||||
|
||||
if time_since_last_call > self.throttle_period:
|
||||
self.time_of_last_call = now
|
||||
return await method(*args, **kwargs)
|
||||
async with self.synchronize:
|
||||
now = datetime.now()
|
||||
time_since_last_call = now - self.time_of_last_call
|
||||
|
||||
if time_since_last_call > self.throttle_period:
|
||||
self.time_of_last_call = now
|
||||
return await method(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
@ -1,21 +1,22 @@
|
||||
"""Validate functions."""
|
||||
import ipaddress
|
||||
import re
|
||||
import uuid
|
||||
import ipaddress
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
ATTR_ACCESS_TOKEN,
|
||||
ATTR_ADDONS_CUSTOM_LIST,
|
||||
ATTR_AUDIO,
|
||||
ATTR_BOOT,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_CLI,
|
||||
ATTR_DEBUG,
|
||||
ATTR_DEBUG_BLOCK,
|
||||
ATTR_DNS,
|
||||
ATTR_HASSIO,
|
||||
ATTR_HASSOS,
|
||||
ATTR_HASSOS_CLI,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_IMAGE,
|
||||
ATTR_LAST_BOOT,
|
||||
@ -36,7 +37,6 @@ from .const import (
|
||||
)
|
||||
from .utils.validate import validate_timezone
|
||||
|
||||
|
||||
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
@ -44,7 +44,6 @@ RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||
network_port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
||||
wait_boot = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
|
||||
docker_image = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
||||
alsa_device = vol.Maybe(vol.Match(r"\d+,\d+"))
|
||||
uuid_match = vol.Match(r"^[0-9a-f]{32}$")
|
||||
sha256 = vol.Match(r"^[0-9a-f]{64}$")
|
||||
token = vol.Match(r"^[0-9a-f]{32,256}$")
|
||||
@ -125,8 +124,9 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
|
||||
vol.Optional(ATTR_HASSIO): vol.Coerce(str),
|
||||
vol.Optional(ATTR_HASSOS): vol.Coerce(str),
|
||||
vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str),
|
||||
vol.Optional(ATTR_CLI): vol.Coerce(str),
|
||||
vol.Optional(ATTR_DNS): vol.Coerce(str),
|
||||
vol.Optional(ATTR_AUDIO): vol.Coerce(str),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
@ -173,3 +173,8 @@ SCHEMA_DNS_CONFIG = vol.Schema(
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_AUDIO_CONFIG = vol.Schema(
|
||||
{vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str))}, extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user