diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d48100d8d..959c5477b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -33,6 +33,14 @@ 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 \ + libpulse0 \ + && 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 \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 02d1af0a8..ff798773c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,31 +1,24 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "name": "Hass.io dev", - "context": "..", - "dockerFile": "Dockerfile", - "appPort": "9123:8123", - "runArgs": [ - "-e", - "GIT_EDITOR=code --wait", - "--privileged" - ], - "extensions": [ - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - "esbenp.prettier-vscode" - ], - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--target-version", - "py37" - ], - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } -} \ No newline at end of file + "name": "Supervisor dev", + "context": "..", + "dockerFile": "Dockerfile", + "appPort": "9123:8123", + "runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"], + "extensions": [ + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + "esbenp.prettier-vscode" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.formatting.blackArgs": ["--target-version", "py37"], + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } +} diff --git a/.dockerignore b/.dockerignore index 0001411df..50bda0041 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,10 +14,10 @@ # virtualenv venv/ -# HA -home-assistant-polymer/* -misc/* -script/* +# Data +home-assistant-polymer/ +script/ +tests/ # Test ENV data/ diff --git a/API.md b/API.md index 0a1fbd328..21470355c 100644 --- a/API.md +++ b/API.md @@ -853,6 +853,99 @@ return: } ``` +### Audio + +- GET `/audio/info` + +```json +{ + "host": "ip-address", + "version": "1", + "latest_version": "2", + "audio": { + "input": [ + { + "name": "...", + "description": "...", + "volume": 0.3, + "default": false + } + ], + "output": [ + { + "name": "...", + "description": "...", + "volume": 0.3, + "default": false + } + ] + } +} +``` + +- POST `/audio/update` + +```json +{ + "version": "VERSION" +} +``` + +- POST `/audio/restart` + +- POST `/audio/reload` + +- GET `/audio/logs` + +- POST `/audio/volume/input` + +```json +{ + "name": "...", + "volume": 0.5 +} +``` + +- POST `/audio/volume/output` + +```json +{ + "name": "...", + "volume": 0.5 +} +``` + +- POST `/audio/default/input` + +```json +{ + "name": "..." +} +``` + +- POST `/audio/default/output` + +```json +{ + "name": "..." +} +``` + +- 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 diff --git a/Dockerfile b/Dockerfile index 5436d632b..91329f10e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,15 @@ FROM $BUILD_FROM # Install base RUN apk add --no-cache \ - openssl \ - libffi \ - musl \ - git \ - socat \ - glib \ eudev \ - eudev-libs + eudev-libs \ + git \ + glib \ + libffi \ + libpulse \ + musl \ + openssl \ + socat ARG BUILD_ARCH WORKDIR /usr/src @@ -23,15 +24,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", "hassio" ] +COPY rootfs / diff --git a/MANIFEST.in b/MANIFEST.in index 34c9c023d..8d25cd1ce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE.md -graft hassio +graft supervisor recursive-exclude * *.py[co] diff --git a/README.md b/README.md index c2428cc77..c6fddb5f1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build Status](https://dev.azure.com/home-assistant/Hass.io/_apis/build/status/hassio?branchName=dev)](https://dev.azure.com/home-assistant/Hass.io/_build/latest?definitionId=2&branchName=dev) -# Hass.io +# Home Assistant Supervisor ## First private cloud solution for home automation @@ -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. -![](misc/hassio.png?raw=true) - ## Installation Installation instructions can be found at . diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 6cee89ace..03e97dcc4 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -17,6 +17,10 @@ jobs: pool: vmImage: "ubuntu-latest" steps: + - script: | + sudo apt-get update + sudo apt-get install -y libpulse0 libudev1 + displayName: "Install Host library" - task: UsePythonVersion@0 displayName: "Use Python 3.7" inputs: diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 69b0e94ac..ee44522f1 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -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" diff --git a/build.json b/build.json new file mode 100644 index 000000000..5dca29692 --- /dev/null +++ b/build.json @@ -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" + } +} diff --git a/entry.sh b/entry.sh deleted file mode 100755 index b46d3d916..000000000 --- a/entry.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -e - -udevd --daemon -udevadm trigger - -if CMD="$(command -v "$1")"; then - shift - exec "$CMD" "$@" -else - echo "Command not found: $1" - exit 1 -fi diff --git a/hassio/__init__.py b/hassio/__init__.py deleted file mode 100644 index e334e96c0..000000000 --- a/hassio/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Init file for Hass.io.""" diff --git a/hassio/data/asound.tmpl b/hassio/data/asound.tmpl deleted file mode 100644 index dc64186fd..000000000 --- a/hassio/data/asound.tmpl +++ /dev/null @@ -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" - } -} diff --git a/hassio/data/audiodb.json b/hassio/data/audiodb.json deleted file mode 100644 index f6cccd456..000000000 --- a/hassio/data/audiodb.json +++ /dev/null @@ -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 - } -} diff --git a/hassio/host/alsa.py b/hassio/host/alsa.py deleted file mode 100644 index e17082364..000000000 --- a/hassio/host/alsa.py +++ /dev/null @@ -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) diff --git a/hassio/misc/__init__.py b/hassio/misc/__init__.py deleted file mode 100644 index 0b6c51de2..000000000 --- a/hassio/misc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Special object and tools for Hass.io.""" diff --git a/misc/hassio.png b/misc/hassio.png deleted file mode 100644 index 3ccbf7ae9..000000000 Binary files a/misc/hassio.png and /dev/null differ diff --git a/misc/hassio.xml b/misc/hassio.xml deleted file mode 100644 index 685610ae1..000000000 --- a/misc/hassio.xml +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4955fb836..55b385836 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,11 +6,13 @@ colorlog==4.1.0 cpe==1.2.1 cryptography==2.8 docker==4.2.0 -gitpython==3.0.7 +gitpython==3.1.0 +jinja2==2.11.1 packaging==20.1 +ptvsd==4.3.2 +pulsectl==20.2.2 pytz==2019.3 pyudev==0.22.0 ruamel.yaml==0.15.100 uvloop==0.14.0 voluptuous==0.11.7 -ptvsd==4.3.2 diff --git a/rootfs/etc/cont-init.d/udev.sh b/rootfs/etc/cont-init.d/udev.sh new file mode 100644 index 000000000..58ea286ea --- /dev/null +++ b/rootfs/etc/cont-init.d/udev.sh @@ -0,0 +1,9 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Start udev service +# ============================================================================== +udevd --daemon + +bashio::log.info "Update udev informations" +udevadm trigger +udevadm settle \ No newline at end of file diff --git a/rootfs/etc/pulse/client.conf b/rootfs/etc/pulse/client.conf new file mode 100644 index 000000000..307143277 --- /dev/null +++ b/rootfs/etc/pulse/client.conf @@ -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 . + +## 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://data/audio/external/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 diff --git a/rootfs/etc/services.d/supervisor/finish b/rootfs/etc/services.d/supervisor/finish new file mode 100644 index 000000000..709660c5a --- /dev/null +++ b/rootfs/etc/services.d/supervisor/finish @@ -0,0 +1,5 @@ +#!/usr/bin/execlineb -S0 +# ============================================================================== +# Take down the S6 supervision tree when Supervisor fails +# ============================================================================== +s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/rootfs/etc/services.d/supervisor/run b/rootfs/etc/services.d/supervisor/run new file mode 100644 index 000000000..4450c0086 --- /dev/null +++ b/rootfs/etc/services.d/supervisor/run @@ -0,0 +1,5 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Start Service service +# ============================================================================== +exec python3 -m supervisor \ No newline at end of file diff --git a/scripts/test_env.sh b/scripts/test_env.sh index c40df18a9..35c09e32b 100755 --- a/scripts/test_env.sh +++ b/scripts/test_env.sh @@ -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 diff --git a/scripts/update-frontend.sh b/scripts/update-frontend.sh index 5610fe7b5..43fef9682 100755 --- a/scripts/update-frontend.sh +++ b/scripts/update-frontend.sh @@ -14,5 +14,5 @@ cd hassio ./script/build_hassio # Copy frontend -rm -f ../../hassio/api/panel/chunk.* -cp -rf build/* ../../hassio/api/panel/ \ No newline at end of file +rm -f ../../supervisor/hassio/api/panel/chunk.* +cp -rf build/* ../../supervisor/api/panel/ \ No newline at end of file diff --git a/setup.py b/setup.py index 0778211e1..85159ee8e 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ +"""Home Assistant Supervisor setup.""" from setuptools import setup -from hassio.const import HASSIO_VERSION +from supervisor.const import SUPERVISOR_VERSION setup( - name="HassIO", - version=HASSIO_VERSION, + name="Supervisor", + version=SUPERVISOR_VERSION, license="BSD License", author="The Home Assistant Authors", author_email="hello@home-assistant.io", @@ -24,19 +25,19 @@ setup( "Topic :: Scientific/Engineering :: Atmospheric Science", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ], keywords=["docker", "home-assistant", "api"], zip_safe=False, platforms="any", packages=[ - "hassio", - "hassio.docker", - "hassio.addons", - "hassio.api", - "hassio.misc", - "hassio.utils", - "hassio.snapshots", + "supervisor", + "supervisor.docker", + "supervisor.addons", + "supervisor.api", + "supervisor.misc", + "supervisor.utils", + "supervisor.snapshots", ], include_package_data=True, ) diff --git a/supervisor/__init__.py b/supervisor/__init__.py new file mode 100644 index 000000000..5b5816e1e --- /dev/null +++ b/supervisor/__init__.py @@ -0,0 +1 @@ +"""Init file for Supervisor.""" diff --git a/hassio/__main__.py b/supervisor/__main__.py similarity index 79% rename from hassio/__main__.py rename to supervisor/__main__.py index dd8a15aa0..806292940 100644 --- a/hassio/__main__.py +++ b/supervisor/__main__.py @@ -1,10 +1,10 @@ -"""Main file for Hass.io.""" +"""Main file for Supervisor.""" import asyncio from concurrent.futures import ThreadPoolExecutor import logging import sys -from hassio import bootstrap +from supervisor import bootstrap _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ if __name__ == "__main__": # Init async event loop loop = initialize_event_loop() - # Check if all information are available to setup Hass.io + # Check if all information are available to setup Supervisor if not bootstrap.check_environment(): sys.exit(1) @@ -37,27 +37,27 @@ if __name__ == "__main__": executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker") loop.set_default_executor(executor) - _LOGGER.info("Initialize Hass.io setup") + _LOGGER.info("Initialize Supervisor setup") coresys = loop.run_until_complete(bootstrap.initialize_coresys()) loop.run_until_complete(coresys.core.connect()) bootstrap.supervisor_debugger(coresys) bootstrap.migrate_system_env(coresys) - _LOGGER.info("Setup HassIO") + _LOGGER.info("Setup Supervisor") loop.run_until_complete(coresys.core.setup()) loop.call_soon_threadsafe(loop.create_task, coresys.core.start()) loop.call_soon_threadsafe(bootstrap.reg_signal, loop) try: - _LOGGER.info("Run Hass.io") + _LOGGER.info("Run Supervisor") loop.run_forever() finally: - _LOGGER.info("Stopping Hass.io") + _LOGGER.info("Stopping Supervisor") loop.run_until_complete(coresys.core.stop()) executor.shutdown(wait=False) loop.close() - _LOGGER.info("Close Hass.io") + _LOGGER.info("Close Supervisor") sys.exit(0) diff --git a/hassio/addons/__init__.py b/supervisor/addons/__init__.py similarity index 98% rename from hassio/addons/__init__.py rename to supervisor/addons/__init__.py index 3e2a18eb6..be2139830 100644 --- a/hassio/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-ons.""" +"""Init file for Supervisor add-ons.""" import asyncio from contextlib import suppress import logging @@ -25,7 +25,7 @@ AnyAddon = Union[Addon, AddonStore] class AddonManager(CoreSysAttributes): - """Manage add-ons inside Hass.io.""" + """Manage add-ons inside Supervisor.""" def __init__(self, coresys: CoreSys): """Initialize Docker base wrapper.""" @@ -57,7 +57,7 @@ class AddonManager(CoreSysAttributes): return self.store.get(addon_slug) def from_token(self, token: str) -> Optional[Addon]: - """Return an add-on from Hass.io token.""" + """Return an add-on from Supervisor token.""" for addon in self.installed: if token == addon.hassio_token: return addon @@ -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): diff --git a/hassio/addons/addon.py b/supervisor/addons/addon.py similarity index 94% rename from hassio/addons/addon.py rename to supervisor/addons/addon.py index 429a2e12f..da0f4ac30 100644 --- a/hassio/addons/addon.py +++ b/supervisor/addons/addon.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-ons.""" +"""Init file for Supervisor add-ons.""" from contextlib import suppress from copy import deepcopy from ipaddress import IPv4Address @@ -65,7 +65,7 @@ RE_WEBUI = re.compile( class Addon(AddonModel): - """Hold data for add-on inside Hass.io.""" + """Hold data for add-on inside Supervisor.""" def __init__(self, coresys: CoreSys, slug: str): """Initialize data holder.""" @@ -163,12 +163,12 @@ class Addon(AddonModel): @property def hassio_token(self) -> Optional[str]: - """Return access token for Hass.io API.""" + """Return access token for Supervisor API.""" return self.persist.get(ATTR_ACCESS_TOKEN) @property def ingress_token(self) -> Optional[str]: - """Return access token for Hass.io API.""" + """Return access token for Supervisor API.""" return self.persist.get(ATTR_INGRESS_TOKEN) @property @@ -250,7 +250,7 @@ class Addon(AddonModel): # lookup the correct protocol from config if t_proto: - proto = "https" if self.options[t_proto] else "http" + proto = "https" if self.options.get(t_proto) else "http" else: proto = s_prefix @@ -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: diff --git a/hassio/addons/build.py b/supervisor/addons/build.py similarity index 96% rename from hassio/addons/build.py rename to supervisor/addons/build.py index 21a2e4c94..d349b60c6 100644 --- a/hassio/addons/build.py +++ b/supervisor/addons/build.py @@ -1,4 +1,4 @@ -"""Hass.io add-on build environment.""" +"""Supervisor add-on build environment.""" from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Dict @@ -16,7 +16,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes): """Handle build options for add-ons.""" def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None: - """Initialize Hass.io add-on builder.""" + """Initialize Supervisor add-on builder.""" self.coresys: CoreSys = coresys self.addon = addon diff --git a/hassio/addons/data.py b/supervisor/addons/data.py similarity index 95% rename from hassio/addons/data.py rename to supervisor/addons/data.py index f94e2af95..b8000b8e3 100644 --- a/hassio/addons/data.py +++ b/supervisor/addons/data.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-on data.""" +"""Init file for Supervisor add-on data.""" from copy import deepcopy import logging from typing import Any, Dict @@ -23,7 +23,7 @@ Config = Dict[str, Any] class AddonsData(JsonConfig, CoreSysAttributes): - """Hold data for installed Add-ons inside Hass.io.""" + """Hold data for installed Add-ons inside Supervisor.""" def __init__(self, coresys: CoreSys): """Initialize data holder.""" diff --git a/hassio/addons/model.py b/supervisor/addons/model.py similarity index 98% rename from hassio/addons/model.py rename to supervisor/addons/model.py index debc0ebe8..c4a15f805 100644 --- a/hassio/addons/model.py +++ b/supervisor/addons/model.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-ons.""" +"""Init file for Supervisor add-ons.""" from pathlib import Path from typing import Any, Awaitable, Dict, List, Optional @@ -137,12 +137,12 @@ class AddonModel(CoreSysAttributes): @property def hassio_token(self) -> Optional[str]: - """Return access token for Hass.io API.""" + """Return access token for Supervisor API.""" return None @property def ingress_token(self) -> Optional[str]: - """Return access token for Hass.io API.""" + """Return access token for Supervisor API.""" return None @property @@ -326,7 +326,7 @@ class AddonModel(CoreSysAttributes): @property def access_hassio_api(self) -> bool: - """Return True if the add-on access to Hass.io REASTful API.""" + """Return True if the add-on access to Supervisor REASTful API.""" return self.data[ATTR_HASSIO_API] @property @@ -336,7 +336,7 @@ class AddonModel(CoreSysAttributes): @property def hassio_role(self) -> str: - """Return Hass.io role for API.""" + """Return Supervisor role for API.""" return self.data[ATTR_HASSIO_ROLE] @property diff --git a/hassio/addons/utils.py b/supervisor/addons/utils.py similarity index 98% rename from hassio/addons/utils.py rename to supervisor/addons/utils.py index 4707df425..434dc7724 100644 --- a/hassio/addons/utils.py +++ b/supervisor/addons/utils.py @@ -59,7 +59,7 @@ def rating_security(addon: AddonModel) -> int: ): rating += -1 - # API Hass.io role + # API Supervisor role if addon.hassio_role == ROLE_MANAGER: rating += -1 elif addon.hassio_role == ROLE_ADMIN: diff --git a/hassio/addons/validate.py b/supervisor/addons/validate.py similarity index 99% rename from hassio/addons/validate.py rename to supervisor/addons/validate.py index ec42060f7..bda65bd84 100644 --- a/hassio/addons/validate.py +++ b/supervisor/addons/validate.py @@ -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(), }, diff --git a/hassio/api/__init__.py b/supervisor/api/__init__.py similarity index 92% rename from hassio/api/__init__.py rename to supervisor/api/__init__.py index 456ff8e7e..bbf1ebecd 100644 --- a/hassio/api/__init__.py +++ b/supervisor/api/__init__.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io RESTful API.""" +"""Init file for Supervisor RESTful API.""" import logging from pathlib import Path from typing import Optional @@ -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__) @@ -29,7 +30,7 @@ MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 class RestAPI(CoreSysAttributes): - """Handle RESTful API for Hass.io.""" + """Handle RESTful API for Supervisor.""" def __init__(self, coresys: CoreSys): """Initialize Docker base wrapper.""" @@ -61,6 +62,7 @@ class RestAPI(CoreSysAttributes): self._register_info() self._register_auth() self._register_dns() + self._register_audio() def _register_host(self) -> None: """Register hostcontrol functions.""" @@ -93,7 +95,7 @@ class RestAPI(CoreSysAttributes): web.post("/os/update", api_hassos.update), web.post("/os/update/cli", api_hassos.update_cli), web.post("/os/config/sync", api_hassos.config_sync), - # Remove with old Hass.io fallback + # Remove with old Supervisor fallback web.get("/hassos/info", api_hassos.info), web.post("/hassos/update", api_hassos.update), web.post("/hassos/update/cli", api_hassos.update_cli), @@ -165,7 +167,7 @@ class RestAPI(CoreSysAttributes): web.post("/core/start", api_hass.start), web.post("/core/check", api_hass.check), web.post("/core/rebuild", api_hass.rebuild), - # Remove with old Hass.io fallback + # Remove with old Supervisor fallback web.get("/homeassistant/info", api_hass.info), web.get("/homeassistant/logs", api_hass.logs), web.get("/homeassistant/stats", api_hass.stats), @@ -192,7 +194,7 @@ class RestAPI(CoreSysAttributes): web.post("/core/api/{path:.+}", api_proxy.api), web.get("/core/api/{path:.+}", api_proxy.api), web.get("/core/api/", api_proxy.api), - # Remove with old Hass.io fallback + # Remove with old Supervisor fallback web.get("/homeassistant/api/websocket", api_proxy.websocket), web.get("/homeassistant/websocket", api_proxy.websocket), web.get("/homeassistant/api/stream", api_proxy.stream), @@ -314,6 +316,24 @@ 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), + web.post("/audio/reload", api_audio.reload), + web.post("/audio/volume/{source}", api_audio.set_volume), + web.post("/audio/default/{source}", api_audio.set_default), + ] + ) + def _register_panel(self) -> None: """Register panel for Home Assistant.""" panel_dir = Path(__file__).parent.joinpath("panel") diff --git a/hassio/api/addons.py b/supervisor/api/addons.py similarity index 98% rename from hassio/api/addons.py rename to supervisor/api/addons.py index 49cdff062..8a11509cf 100644 --- a/hassio/api/addons.py +++ b/supervisor/api/addons.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Home Assistant RESTful API.""" +"""Init file for Supervisor Home Assistant RESTful API.""" import asyncio import logging from typing import Any, Awaitable, Dict, List @@ -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(), } ) diff --git a/supervisor/api/audio.py b/supervisor/api/audio.py new file mode 100644 index 000000000..b8e9357a8 --- /dev/null +++ b/supervisor/api/audio.py @@ -0,0 +1,127 @@ +"""Init file for Supervisor Audio RESTful API.""" +import asyncio +import logging +from typing import Any, Awaitable, Dict + +from aiohttp import web +import attr +import voluptuous as vol + +from ..const import ( + ATTR_AUDIO, + ATTR_BLK_READ, + ATTR_BLK_WRITE, + ATTR_CPU_PERCENT, + ATTR_HOST, + ATTR_INPUT, + ATTR_LATEST_VERSION, + ATTR_MEMORY_LIMIT, + ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, + ATTR_NAME, + ATTR_NETWORK_RX, + ATTR_NETWORK_TX, + ATTR_OUTPUT, + ATTR_VERSION, + ATTR_VOLUME, + CONTENT_TYPE_BINARY, +) +from ..coresys import CoreSysAttributes +from ..exceptions import APIError +from ..host.sound import SourceType +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)}) + +SCHEMA_VOLUME = vol.Schema( + { + vol.Required(ATTR_NAME): vol.Coerce(str), + vol.Required(ATTR_VOLUME): vol.Coerce(float), + } +) + +SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): 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), + ATTR_AUDIO: { + ATTR_INPUT: [ + attr.asdict(profile) + for profile in self.sys_host.sound.input_profiles + ], + ATTR_OUTPUT: [ + attr.asdict(profile) + for profile in self.sys_host.sound.output_profiles + ], + }, + } + + @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()) + + @api_process + def reload(self, request: web.Request) -> Awaitable[None]: + """Reload Audio information.""" + return asyncio.shield(self.sys_host.sound.update()) + + @api_process + async def set_volume(self, request: web.Request) -> None: + """Set Audio information.""" + source: SourceType = SourceType(request.match_info.get("source")) + body = await api_validate(SCHEMA_VOLUME, request) + + await asyncio.shield( + self.sys_host.sound.set_volume(source, body[ATTR_NAME], body[ATTR_VOLUME]) + ) + + @api_process + async def set_default(self, request: web.Request) -> None: + """Set Audio default sources.""" + source: SourceType = SourceType(request.match_info.get("source")) + body = await api_validate(SCHEMA_DEFAULT, request) + + await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME])) diff --git a/hassio/api/auth.py b/supervisor/api/auth.py similarity index 94% rename from hassio/api/auth.py rename to supervisor/api/auth.py index 5a645f129..7143fbbad 100644 --- a/hassio/api/auth.py +++ b/supervisor/api/auth.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io auth/SSO RESTful API.""" +"""Init file for Supervisor auth/SSO RESTful API.""" import asyncio import logging from typing import Dict @@ -76,7 +76,7 @@ class APIAuth(CoreSysAttributes): return await self._process_dict(request, addon, data) raise HTTPUnauthorized( - headers={WWW_AUTHENTICATE: 'Basic realm="Hass.io Authentication"'} + headers={WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'} ) @api_process diff --git a/hassio/api/discovery.py b/supervisor/api/discovery.py similarity index 98% rename from hassio/api/discovery.py rename to supervisor/api/discovery.py index 560f51fa7..ddc03abc5 100644 --- a/hassio/api/discovery.py +++ b/supervisor/api/discovery.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io network RESTful API.""" +"""Init file for Supervisor network RESTful API.""" import voluptuous as vol from .utils import api_process, api_validate diff --git a/hassio/api/dns.py b/supervisor/api/dns.py similarity index 98% rename from hassio/api/dns.py rename to supervisor/api/dns.py index a56e5fca5..998f886ff 100644 --- a/hassio/api/dns.py +++ b/supervisor/api/dns.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io DNS RESTful API.""" +"""Init file for Supervisor DNS RESTful API.""" import asyncio import logging from typing import Any, Awaitable, Dict diff --git a/hassio/api/hardware.py b/supervisor/api/hardware.py similarity index 74% rename from hassio/api/hardware.py rename to supervisor/api/hardware.py index 1d71b67f7..a2bd1e2e8 100644 --- a/hassio/api/hardware.py +++ b/supervisor/api/hardware.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io hardware RESTful API.""" +"""Init file for Supervisor hardware RESTful API.""" import asyncio import logging from typing import Any, Dict @@ -37,11 +37,17 @@ class APIHardware(CoreSysAttributes): @api_process async def audio(self, request: web.Request) -> Dict[str, Any]: - """Show ALSA audio devices.""" + """Show pulse audio profiles.""" return { ATTR_AUDIO: { - ATTR_INPUT: self.sys_host.alsa.input_devices, - ATTR_OUTPUT: self.sys_host.alsa.output_devices, + ATTR_INPUT: { + profile.name: profile.description + for profile in self.sys_host.sound.input_profiles + }, + ATTR_OUTPUT: { + profile.name: profile.description + for profile in self.sys_host.sound.output_profiles + }, } } diff --git a/hassio/api/hassos.py b/supervisor/api/hassos.py similarity index 97% rename from hassio/api/hassos.py rename to supervisor/api/hassos.py index ef5153cb6..95128f525 100644 --- a/hassio/api/hassos.py +++ b/supervisor/api/hassos.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io HassOS RESTful API.""" +"""Init file for Supervisor HassOS RESTful API.""" import asyncio import logging from typing import Any, Awaitable, Dict diff --git a/hassio/api/homeassistant.py b/supervisor/api/homeassistant.py similarity index 98% rename from hassio/api/homeassistant.py rename to supervisor/api/homeassistant.py index 99e72fe2b..d9f691f96 100644 --- a/hassio/api/homeassistant.py +++ b/supervisor/api/homeassistant.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Home Assistant RESTful API.""" +"""Init file for Supervisor Home Assistant RESTful API.""" import asyncio import logging from typing import Coroutine, Dict, Any diff --git a/hassio/api/host.py b/supervisor/api/host.py similarity index 98% rename from hassio/api/host.py rename to supervisor/api/host.py index 6c05ea5b0..d686863c5 100644 --- a/hassio/api/host.py +++ b/supervisor/api/host.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io host RESTful API.""" +"""Init file for Supervisor host RESTful API.""" import asyncio import logging diff --git a/hassio/api/info.py b/supervisor/api/info.py similarity index 95% rename from hassio/api/info.py rename to supervisor/api/info.py index 572552119..7c2914dc5 100644 --- a/hassio/api/info.py +++ b/supervisor/api/info.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io info RESTful API.""" +"""Init file for Supervisor info RESTful API.""" import logging from typing import Any, Dict diff --git a/hassio/api/ingress.py b/supervisor/api/ingress.py similarity index 98% rename from hassio/api/ingress.py rename to supervisor/api/ingress.py index 9d3e12304..2b29b5648 100644 --- a/hassio/api/ingress.py +++ b/supervisor/api/ingress.py @@ -1,4 +1,4 @@ -"""Hass.io Add-on ingress service.""" +"""Supervisor Add-on ingress service.""" import asyncio from ipaddress import ip_address import logging @@ -81,7 +81,7 @@ class APIIngress(CoreSysAttributes): async def handler( self, request: web.Request ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: - """Route data to Hass.io ingress service.""" + """Route data to Supervisor ingress service.""" self._check_ha_access(request) # Check Ingress Session diff --git a/hassio/api/panel/201359fd5a526afe13ef.worker.js b/supervisor/api/panel/201359fd5a526afe13ef.worker.js similarity index 100% rename from hassio/api/panel/201359fd5a526afe13ef.worker.js rename to supervisor/api/panel/201359fd5a526afe13ef.worker.js diff --git a/hassio/api/panel/201359fd5a526afe13ef.worker.js.gz b/supervisor/api/panel/201359fd5a526afe13ef.worker.js.gz similarity index 100% rename from hassio/api/panel/201359fd5a526afe13ef.worker.js.gz rename to supervisor/api/panel/201359fd5a526afe13ef.worker.js.gz diff --git a/hassio/api/panel/201359fd5a526afe13ef.worker.js.map b/supervisor/api/panel/201359fd5a526afe13ef.worker.js.map similarity index 100% rename from hassio/api/panel/201359fd5a526afe13ef.worker.js.map rename to supervisor/api/panel/201359fd5a526afe13ef.worker.js.map diff --git a/hassio/api/panel/chunk.0b82745c7bdffe5c1404.js b/supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js similarity index 100% rename from hassio/api/panel/chunk.0b82745c7bdffe5c1404.js rename to supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js diff --git a/hassio/api/panel/chunk.0b82745c7bdffe5c1404.js.gz b/supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js.gz similarity index 100% rename from hassio/api/panel/chunk.0b82745c7bdffe5c1404.js.gz rename to supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js.gz diff --git a/hassio/api/panel/chunk.0b82745c7bdffe5c1404.js.map b/supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js.map similarity index 100% rename from hassio/api/panel/chunk.0b82745c7bdffe5c1404.js.map rename to supervisor/api/panel/chunk.0b82745c7bdffe5c1404.js.map diff --git a/hassio/api/panel/chunk.429840c83fad61bc51a8.js b/supervisor/api/panel/chunk.429840c83fad61bc51a8.js similarity index 100% rename from hassio/api/panel/chunk.429840c83fad61bc51a8.js rename to supervisor/api/panel/chunk.429840c83fad61bc51a8.js diff --git a/hassio/api/panel/chunk.429840c83fad61bc51a8.js.LICENSE b/supervisor/api/panel/chunk.429840c83fad61bc51a8.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.429840c83fad61bc51a8.js.LICENSE rename to supervisor/api/panel/chunk.429840c83fad61bc51a8.js.LICENSE diff --git a/hassio/api/panel/chunk.429840c83fad61bc51a8.js.gz b/supervisor/api/panel/chunk.429840c83fad61bc51a8.js.gz similarity index 100% rename from hassio/api/panel/chunk.429840c83fad61bc51a8.js.gz rename to supervisor/api/panel/chunk.429840c83fad61bc51a8.js.gz diff --git a/hassio/api/panel/chunk.429840c83fad61bc51a8.js.map b/supervisor/api/panel/chunk.429840c83fad61bc51a8.js.map similarity index 100% rename from hassio/api/panel/chunk.429840c83fad61bc51a8.js.map rename to supervisor/api/panel/chunk.429840c83fad61bc51a8.js.map diff --git a/hassio/api/panel/chunk.43e40fd69686ad51301d.js b/supervisor/api/panel/chunk.43e40fd69686ad51301d.js similarity index 100% rename from hassio/api/panel/chunk.43e40fd69686ad51301d.js rename to supervisor/api/panel/chunk.43e40fd69686ad51301d.js diff --git a/hassio/api/panel/chunk.43e40fd69686ad51301d.js.gz b/supervisor/api/panel/chunk.43e40fd69686ad51301d.js.gz similarity index 100% rename from hassio/api/panel/chunk.43e40fd69686ad51301d.js.gz rename to supervisor/api/panel/chunk.43e40fd69686ad51301d.js.gz diff --git a/hassio/api/panel/chunk.43e40fd69686ad51301d.js.map b/supervisor/api/panel/chunk.43e40fd69686ad51301d.js.map similarity index 100% rename from hassio/api/panel/chunk.43e40fd69686ad51301d.js.map rename to supervisor/api/panel/chunk.43e40fd69686ad51301d.js.map diff --git a/hassio/api/panel/chunk.4d45ee0a3d852768f97e.js b/supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js similarity index 100% rename from hassio/api/panel/chunk.4d45ee0a3d852768f97e.js rename to supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js diff --git a/hassio/api/panel/chunk.4d45ee0a3d852768f97e.js.gz b/supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js.gz similarity index 100% rename from hassio/api/panel/chunk.4d45ee0a3d852768f97e.js.gz rename to supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js.gz diff --git a/hassio/api/panel/chunk.4d45ee0a3d852768f97e.js.map b/supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js.map similarity index 100% rename from hassio/api/panel/chunk.4d45ee0a3d852768f97e.js.map rename to supervisor/api/panel/chunk.4d45ee0a3d852768f97e.js.map diff --git a/hassio/api/panel/chunk.715824f4764bdbe425b1.js b/supervisor/api/panel/chunk.715824f4764bdbe425b1.js similarity index 100% rename from hassio/api/panel/chunk.715824f4764bdbe425b1.js rename to supervisor/api/panel/chunk.715824f4764bdbe425b1.js diff --git a/hassio/api/panel/chunk.715824f4764bdbe425b1.js.LICENSE b/supervisor/api/panel/chunk.715824f4764bdbe425b1.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.715824f4764bdbe425b1.js.LICENSE rename to supervisor/api/panel/chunk.715824f4764bdbe425b1.js.LICENSE diff --git a/hassio/api/panel/chunk.715824f4764bdbe425b1.js.gz b/supervisor/api/panel/chunk.715824f4764bdbe425b1.js.gz similarity index 100% rename from hassio/api/panel/chunk.715824f4764bdbe425b1.js.gz rename to supervisor/api/panel/chunk.715824f4764bdbe425b1.js.gz diff --git a/hassio/api/panel/chunk.715824f4764bdbe425b1.js.map b/supervisor/api/panel/chunk.715824f4764bdbe425b1.js.map similarity index 100% rename from hassio/api/panel/chunk.715824f4764bdbe425b1.js.map rename to supervisor/api/panel/chunk.715824f4764bdbe425b1.js.map diff --git a/hassio/api/panel/chunk.8527374a266cecf93aa9.js b/supervisor/api/panel/chunk.8527374a266cecf93aa9.js similarity index 100% rename from hassio/api/panel/chunk.8527374a266cecf93aa9.js rename to supervisor/api/panel/chunk.8527374a266cecf93aa9.js diff --git a/hassio/api/panel/chunk.8527374a266cecf93aa9.js.gz b/supervisor/api/panel/chunk.8527374a266cecf93aa9.js.gz similarity index 100% rename from hassio/api/panel/chunk.8527374a266cecf93aa9.js.gz rename to supervisor/api/panel/chunk.8527374a266cecf93aa9.js.gz diff --git a/hassio/api/panel/chunk.8527374a266cecf93aa9.js.map b/supervisor/api/panel/chunk.8527374a266cecf93aa9.js.map similarity index 100% rename from hassio/api/panel/chunk.8527374a266cecf93aa9.js.map rename to supervisor/api/panel/chunk.8527374a266cecf93aa9.js.map diff --git a/hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js b/supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js similarity index 100% rename from hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js rename to supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js diff --git a/hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE b/supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE rename to supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE diff --git a/hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz b/supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz similarity index 100% rename from hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz rename to supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz diff --git a/hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.map b/supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.map similarity index 100% rename from hassio/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.map rename to supervisor/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.map diff --git a/hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js b/supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js similarity index 100% rename from hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js rename to supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js diff --git a/hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js.gz b/supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js.gz similarity index 100% rename from hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js.gz rename to supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js.gz diff --git a/hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js.map b/supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js.map similarity index 100% rename from hassio/api/panel/chunk.92a11ac1b80e0d7839d2.js.map rename to supervisor/api/panel/chunk.92a11ac1b80e0d7839d2.js.map diff --git a/hassio/api/panel/chunk.990ee58006b248f55d23.js b/supervisor/api/panel/chunk.990ee58006b248f55d23.js similarity index 100% rename from hassio/api/panel/chunk.990ee58006b248f55d23.js rename to supervisor/api/panel/chunk.990ee58006b248f55d23.js diff --git a/hassio/api/panel/chunk.990ee58006b248f55d23.js.gz b/supervisor/api/panel/chunk.990ee58006b248f55d23.js.gz similarity index 100% rename from hassio/api/panel/chunk.990ee58006b248f55d23.js.gz rename to supervisor/api/panel/chunk.990ee58006b248f55d23.js.gz diff --git a/hassio/api/panel/chunk.990ee58006b248f55d23.js.map b/supervisor/api/panel/chunk.990ee58006b248f55d23.js.map similarity index 100% rename from hassio/api/panel/chunk.990ee58006b248f55d23.js.map rename to supervisor/api/panel/chunk.990ee58006b248f55d23.js.map diff --git a/hassio/api/panel/chunk.9d371c8143226d4eaaee.js b/supervisor/api/panel/chunk.9d371c8143226d4eaaee.js similarity index 100% rename from hassio/api/panel/chunk.9d371c8143226d4eaaee.js rename to supervisor/api/panel/chunk.9d371c8143226d4eaaee.js diff --git a/hassio/api/panel/chunk.9d371c8143226d4eaaee.js.LICENSE b/supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.9d371c8143226d4eaaee.js.LICENSE rename to supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.LICENSE diff --git a/hassio/api/panel/chunk.9d371c8143226d4eaaee.js.gz b/supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.gz similarity index 100% rename from hassio/api/panel/chunk.9d371c8143226d4eaaee.js.gz rename to supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.gz diff --git a/hassio/api/panel/chunk.9d371c8143226d4eaaee.js.map b/supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.map similarity index 100% rename from hassio/api/panel/chunk.9d371c8143226d4eaaee.js.map rename to supervisor/api/panel/chunk.9d371c8143226d4eaaee.js.map diff --git a/hassio/api/panel/chunk.b2dce600432c76a53d8c.js b/supervisor/api/panel/chunk.b2dce600432c76a53d8c.js similarity index 100% rename from hassio/api/panel/chunk.b2dce600432c76a53d8c.js rename to supervisor/api/panel/chunk.b2dce600432c76a53d8c.js diff --git a/hassio/api/panel/chunk.b2dce600432c76a53d8c.js.gz b/supervisor/api/panel/chunk.b2dce600432c76a53d8c.js.gz similarity index 100% rename from hassio/api/panel/chunk.b2dce600432c76a53d8c.js.gz rename to supervisor/api/panel/chunk.b2dce600432c76a53d8c.js.gz diff --git a/hassio/api/panel/chunk.b2dce600432c76a53d8c.js.map b/supervisor/api/panel/chunk.b2dce600432c76a53d8c.js.map similarity index 100% rename from hassio/api/panel/chunk.b2dce600432c76a53d8c.js.map rename to supervisor/api/panel/chunk.b2dce600432c76a53d8c.js.map diff --git a/hassio/api/panel/chunk.b60200a57d6f63941b30.js b/supervisor/api/panel/chunk.b60200a57d6f63941b30.js similarity index 100% rename from hassio/api/panel/chunk.b60200a57d6f63941b30.js rename to supervisor/api/panel/chunk.b60200a57d6f63941b30.js diff --git a/hassio/api/panel/chunk.b60200a57d6f63941b30.js.gz b/supervisor/api/panel/chunk.b60200a57d6f63941b30.js.gz similarity index 100% rename from hassio/api/panel/chunk.b60200a57d6f63941b30.js.gz rename to supervisor/api/panel/chunk.b60200a57d6f63941b30.js.gz diff --git a/hassio/api/panel/chunk.b60200a57d6f63941b30.js.map b/supervisor/api/panel/chunk.b60200a57d6f63941b30.js.map similarity index 100% rename from hassio/api/panel/chunk.b60200a57d6f63941b30.js.map rename to supervisor/api/panel/chunk.b60200a57d6f63941b30.js.map diff --git a/hassio/api/panel/chunk.d4931d72592ad48ba2be.js b/supervisor/api/panel/chunk.d4931d72592ad48ba2be.js similarity index 100% rename from hassio/api/panel/chunk.d4931d72592ad48ba2be.js rename to supervisor/api/panel/chunk.d4931d72592ad48ba2be.js diff --git a/hassio/api/panel/chunk.d4931d72592ad48ba2be.js.LICENSE b/supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.d4931d72592ad48ba2be.js.LICENSE rename to supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.LICENSE diff --git a/hassio/api/panel/chunk.d4931d72592ad48ba2be.js.gz b/supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.gz similarity index 100% rename from hassio/api/panel/chunk.d4931d72592ad48ba2be.js.gz rename to supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.gz diff --git a/hassio/api/panel/chunk.d4931d72592ad48ba2be.js.map b/supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.map similarity index 100% rename from hassio/api/panel/chunk.d4931d72592ad48ba2be.js.map rename to supervisor/api/panel/chunk.d4931d72592ad48ba2be.js.map diff --git a/hassio/api/panel/chunk.e46c606dd9100816af4e.js b/supervisor/api/panel/chunk.e46c606dd9100816af4e.js similarity index 100% rename from hassio/api/panel/chunk.e46c606dd9100816af4e.js rename to supervisor/api/panel/chunk.e46c606dd9100816af4e.js diff --git a/hassio/api/panel/chunk.e46c606dd9100816af4e.js.LICENSE b/supervisor/api/panel/chunk.e46c606dd9100816af4e.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.e46c606dd9100816af4e.js.LICENSE rename to supervisor/api/panel/chunk.e46c606dd9100816af4e.js.LICENSE diff --git a/hassio/api/panel/chunk.e46c606dd9100816af4e.js.gz b/supervisor/api/panel/chunk.e46c606dd9100816af4e.js.gz similarity index 100% rename from hassio/api/panel/chunk.e46c606dd9100816af4e.js.gz rename to supervisor/api/panel/chunk.e46c606dd9100816af4e.js.gz diff --git a/hassio/api/panel/chunk.e46c606dd9100816af4e.js.map b/supervisor/api/panel/chunk.e46c606dd9100816af4e.js.map similarity index 100% rename from hassio/api/panel/chunk.e46c606dd9100816af4e.js.map rename to supervisor/api/panel/chunk.e46c606dd9100816af4e.js.map diff --git a/hassio/api/panel/chunk.f49e500cf58ea310d452.js b/supervisor/api/panel/chunk.f49e500cf58ea310d452.js similarity index 100% rename from hassio/api/panel/chunk.f49e500cf58ea310d452.js rename to supervisor/api/panel/chunk.f49e500cf58ea310d452.js diff --git a/hassio/api/panel/chunk.f49e500cf58ea310d452.js.LICENSE b/supervisor/api/panel/chunk.f49e500cf58ea310d452.js.LICENSE similarity index 100% rename from hassio/api/panel/chunk.f49e500cf58ea310d452.js.LICENSE rename to supervisor/api/panel/chunk.f49e500cf58ea310d452.js.LICENSE diff --git a/hassio/api/panel/chunk.f49e500cf58ea310d452.js.gz b/supervisor/api/panel/chunk.f49e500cf58ea310d452.js.gz similarity index 100% rename from hassio/api/panel/chunk.f49e500cf58ea310d452.js.gz rename to supervisor/api/panel/chunk.f49e500cf58ea310d452.js.gz diff --git a/hassio/api/panel/chunk.f49e500cf58ea310d452.js.map b/supervisor/api/panel/chunk.f49e500cf58ea310d452.js.map similarity index 100% rename from hassio/api/panel/chunk.f49e500cf58ea310d452.js.map rename to supervisor/api/panel/chunk.f49e500cf58ea310d452.js.map diff --git a/hassio/api/panel/entrypoint.582baa2f.js b/supervisor/api/panel/entrypoint.582baa2f.js similarity index 100% rename from hassio/api/panel/entrypoint.582baa2f.js rename to supervisor/api/panel/entrypoint.582baa2f.js diff --git a/hassio/api/panel/entrypoint.582baa2f.js.gz b/supervisor/api/panel/entrypoint.582baa2f.js.gz similarity index 100% rename from hassio/api/panel/entrypoint.582baa2f.js.gz rename to supervisor/api/panel/entrypoint.582baa2f.js.gz diff --git a/hassio/api/panel/entrypoint.582baa2f.js.map b/supervisor/api/panel/entrypoint.582baa2f.js.map similarity index 100% rename from hassio/api/panel/entrypoint.582baa2f.js.map rename to supervisor/api/panel/entrypoint.582baa2f.js.map diff --git a/hassio/api/panel/entrypoint.js b/supervisor/api/panel/entrypoint.js similarity index 100% rename from hassio/api/panel/entrypoint.js rename to supervisor/api/panel/entrypoint.js diff --git a/hassio/api/panel/entrypoint.js.gz b/supervisor/api/panel/entrypoint.js.gz similarity index 100% rename from hassio/api/panel/entrypoint.js.gz rename to supervisor/api/panel/entrypoint.js.gz diff --git a/hassio/api/panel/entrypoint.js.map b/supervisor/api/panel/entrypoint.js.map similarity index 100% rename from hassio/api/panel/entrypoint.js.map rename to supervisor/api/panel/entrypoint.js.map diff --git a/hassio/api/panel/index.html b/supervisor/api/panel/index.html similarity index 100% rename from hassio/api/panel/index.html rename to supervisor/api/panel/index.html diff --git a/hassio/api/panel/index.html.gz b/supervisor/api/panel/index.html.gz similarity index 100% rename from hassio/api/panel/index.html.gz rename to supervisor/api/panel/index.html.gz diff --git a/hassio/api/panel/manifest.json b/supervisor/api/panel/manifest.json similarity index 100% rename from hassio/api/panel/manifest.json rename to supervisor/api/panel/manifest.json diff --git a/hassio/api/proxy.py b/supervisor/api/proxy.py similarity index 99% rename from hassio/api/proxy.py rename to supervisor/api/proxy.py index cf8a8abe3..9e8204301 100644 --- a/hassio/api/proxy.py +++ b/supervisor/api/proxy.py @@ -23,7 +23,7 @@ class APIProxy(CoreSysAttributes): """API Proxy for Home Assistant.""" def _check_access(self, request: web.Request): - """Check the Hass.io token.""" + """Check the Supervisor token.""" if AUTHORIZATION in request.headers: bearer = request.headers[AUTHORIZATION] hassio_token = bearer.split(" ")[-1] diff --git a/hassio/api/security.py b/supervisor/api/security.py similarity index 99% rename from hassio/api/security.py rename to supervisor/api/security.py index 9ce4661dd..e338600be 100644 --- a/hassio/api/security.py +++ b/supervisor/api/security.py @@ -73,6 +73,7 @@ ADDONS_ROLE_ACCESS = { ), ROLE_MANAGER: re.compile( r"^(?:" + r"|/audio/.*" r"|/dns/.*" r"|/core/.+" r"|/homeassistant/.+" diff --git a/hassio/api/services.py b/supervisor/api/services.py similarity index 97% rename from hassio/api/services.py rename to supervisor/api/services.py index 53090cb38..25e964d93 100644 --- a/hassio/api/services.py +++ b/supervisor/api/services.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io network RESTful API.""" +"""Init file for Supervisor network RESTful API.""" from .utils import api_process, api_validate from ..const import ( diff --git a/hassio/api/snapshots.py b/supervisor/api/snapshots.py similarity index 99% rename from hassio/api/snapshots.py rename to supervisor/api/snapshots.py index e2529df52..152c820aa 100644 --- a/hassio/api/snapshots.py +++ b/supervisor/api/snapshots.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io snapshot RESTful API.""" +"""Init file for Supervisor snapshot RESTful API.""" import asyncio import logging from pathlib import Path diff --git a/hassio/api/supervisor.py b/supervisor/api/supervisor.py similarity index 97% rename from hassio/api/supervisor.py rename to supervisor/api/supervisor.py index b77aebc24..d4d54cefe 100644 --- a/hassio/api/supervisor.py +++ b/supervisor/api/supervisor.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Supervisor RESTful API.""" +"""Init file for Supervisor Supervisor RESTful API.""" import asyncio import logging from typing import Any, Awaitable, Dict @@ -36,7 +36,7 @@ from ..const import ( ATTR_VERSION, ATTR_WAIT_BOOT, CONTENT_TYPE_BINARY, - HASSIO_VERSION, + SUPERVISOR_VERSION, UpdateChannels, ) from ..coresys import CoreSysAttributes @@ -91,7 +91,7 @@ class APISupervisor(CoreSysAttributes): ) return { - ATTR_VERSION: HASSIO_VERSION, + ATTR_VERSION: SUPERVISOR_VERSION, ATTR_LAST_VERSION: self.sys_updater.version_hassio, ATTR_CHANNEL: self.sys_updater.channel, ATTR_ARCH: self.sys_supervisor.arch, diff --git a/hassio/api/utils.py b/supervisor/api/utils.py similarity index 97% rename from hassio/api/utils.py rename to supervisor/api/utils.py index 8432d47ee..f44966107 100644 --- a/hassio/api/utils.py +++ b/supervisor/api/utils.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io util for RESTful API.""" +"""Init file for Supervisor util for RESTful API.""" import json import logging from typing import Any, Dict, List, Optional @@ -29,7 +29,7 @@ def excract_supervisor_token(request: web.Request) -> Optional[str]: if supervisor_token: return supervisor_token - # Remove with old Hass.io fallback + # Remove with old Supervisor fallback supervisor_token = request.headers.get(HEADER_TOKEN_OLD) if supervisor_token: return supervisor_token diff --git a/hassio/arch.py b/supervisor/arch.py similarity index 100% rename from hassio/arch.py rename to supervisor/arch.py diff --git a/supervisor/audio.py b/supervisor/audio.py new file mode 100644 index 000000000..93c6fc6ce --- /dev/null +++ b/supervisor/audio.py @@ -0,0 +1,202 @@ +"""Home Assistant control object.""" +import asyncio +from contextlib import suppress +import logging +from pathlib import Path +from typing import Awaitable, Optional + +import jinja2 + +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) + self.client_template: Optional[jinja2.Template] = None + + @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 PulseAudio + with suppress(AudioError): + await self.install() + else: + self.version = self.instance.version + self.save_data() + + # Run PulseAudio + with suppress(AudioError): + if await self.instance.is_running(): + await self.restart() + else: + await self.start() + + # Initialize Client Template + try: + self.client_template = jinja2.Template(PULSE_CLIENT_TMPL.read_text()) + except OSError as err: + _LOGGER.error("Can't read pulse-client.tmpl: %s", err) + + 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.""" + if self.client_template is None: + return "" + + # Process Template + return self.client_template.render( + audio_address=self.sys_docker.network.audio, + default_source=input_profile, + default_sink=output_profile, + ) diff --git a/hassio/auth.py b/supervisor/auth.py similarity index 100% rename from hassio/auth.py rename to supervisor/auth.py diff --git a/hassio/bootstrap.py b/supervisor/bootstrap.py similarity index 82% rename from hassio/bootstrap.py rename to supervisor/bootstrap.py index ca9e930ca..4a47dc78b 100644 --- a/hassio/bootstrap.py +++ b/supervisor/bootstrap.py @@ -1,4 +1,4 @@ -"""Bootstrap Hass.io.""" +"""Bootstrap Supervisor.""" import logging import os from pathlib import Path @@ -11,8 +11,9 @@ 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 HassIO +from .core import Core from .coresys import CoreSys from .dbus import DBusManager from .discovery import Discovery @@ -40,13 +41,14 @@ MACHINE_ID = Path("/etc/machine-id") async def initialize_coresys(): - """Initialize HassIO coresys/objects.""" + """Initialize supervisor coresys/objects.""" coresys = CoreSys() # Initialize core objects - coresys.core = HassIO(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,53 +91,60 @@ def initialize_system_data(coresys: CoreSys): ) config.path_homeassistant.mkdir() - # hassio ssl folder + # Supervisor ssl folder if not config.path_ssl.is_dir(): - _LOGGER.info("Create Hass.io SSL/TLS folder %s", config.path_ssl) + _LOGGER.info("Create Supervisor SSL/TLS folder %s", config.path_ssl) config.path_ssl.mkdir() - # hassio addon data folder + # Supervisor addon data folder if not config.path_addons_data.is_dir(): - _LOGGER.info("Create Hass.io Add-on data folder %s", config.path_addons_data) + _LOGGER.info("Create Supervisor Add-on data folder %s", config.path_addons_data) config.path_addons_data.mkdir(parents=True) if not config.path_addons_local.is_dir(): _LOGGER.info( - "Create Hass.io Add-on local repository folder %s", config.path_addons_local + "Create Supervisor Add-on local repository folder %s", + config.path_addons_local, ) config.path_addons_local.mkdir(parents=True) if not config.path_addons_git.is_dir(): _LOGGER.info( - "Create Hass.io Add-on git repositories folder %s", config.path_addons_git + "Create Supervisor Add-on git repositories folder %s", + config.path_addons_git, ) config.path_addons_git.mkdir(parents=True) - # hassio tmp folder + # Supervisor tmp folder if not config.path_tmp.is_dir(): - _LOGGER.info("Create Hass.io temp folder %s", config.path_tmp) + _LOGGER.info("Create Supervisor temp folder %s", config.path_tmp) config.path_tmp.mkdir(parents=True) - # hassio backup folder + # Supervisor backup folder if not config.path_backup.is_dir(): - _LOGGER.info("Create Hass.io backup folder %s", config.path_backup) + _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 Hass.io share folder %s", config.path_share) + _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 Hass.io Apparmor folder %s", config.path_apparmor) + _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 Hass.io DNS folder %s", config.path_dns) + _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() @@ -239,7 +248,7 @@ def supervisor_debugger(coresys: CoreSys) -> None: # pylint: disable=import-outside-toplevel import ptvsd - _LOGGER.info("Initialize Hass.io debugger") + _LOGGER.info("Initialize Supervisor debugger") ptvsd.enable_attach(address=("0.0.0.0", 33333), redirect_output=True) if coresys.config.debug_block: diff --git a/hassio/config.py b/supervisor/config.py similarity index 83% rename from hassio/config.py rename to supervisor/config.py index 7d6161c17..754e029ba 100644 --- a/hassio/config.py +++ b/supervisor/config.py @@ -1,4 +1,4 @@ -"""Bootstrap Hass.io.""" +"""Bootstrap Supervisor.""" from datetime import datetime import logging import os @@ -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() @@ -100,7 +101,7 @@ class CoreConfig(JsonConfig): def modify_log_level(self) -> None: """Change log level.""" lvl = getattr(logging, self.logging.upper()) - logging.getLogger("hassio").setLevel(lvl) + logging.getLogger("supervisor").setLevel(lvl) @property def last_boot(self): @@ -119,12 +120,12 @@ class CoreConfig(JsonConfig): @property def path_hassio(self): - """Return Hass.io data path.""" - return HASSIO_DATA + """Return Supervisor data path.""" + return SUPERVISOR_DATA @property def path_extern_hassio(self): - """Return Hass.io data path external for Docker.""" + """Return Supervisor data path external for Docker.""" return PurePath(os.environ["SUPERVISOR_SHARE"]) @property @@ -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,27 +171,37 @@ 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 Hass.io temp folder.""" - return Path(HASSIO_DATA, TMP_DATA) + """Return Supervisor temp folder.""" + return Path(SUPERVISOR_DATA, TMP_DATA) @property def path_extern_tmp(self): - """Return Hass.io temp folder for Docker.""" + """Return Supervisor temp folder for Docker.""" return PurePath(self.path_extern_hassio, TMP_DATA) @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): diff --git a/hassio/const.py b/supervisor/const.py similarity index 89% rename from hassio/const.py rename to supervisor/const.py index f840b61d5..e00b2c1fb 100644 --- a/hassio/const.py +++ b/supervisor/const.py @@ -1,9 +1,9 @@ -"""Constants file for Hass.io.""" +"""Constants file for Supervisor.""" from enum import Enum from ipaddress import ip_network from pathlib import Path -HASSIO_VERSION = "201" +SUPERVISOR_VERSION = "202" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" @@ -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,9 @@ ATTR_SNAPSHOT_EXCLUDE = "snapshot_exclude" ATTR_DOCUMENTATION = "documentation" ATTR_ADVANCED = "advanced" ATTR_STAGE = "stage" +ATTR_CLI = "cli" +ATTR_DEFAULT = "default" +ATTR_VOLUME = "volume" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" @@ -342,3 +346,12 @@ class UpdateChannels(str, Enum): STABLE = "stable" BETA = "beta" DEV = "dev" + + +class CoreStates(str, Enum): + """Represent current loading state.""" + + INITIALIZE = "initialize" + STARTUP = "startup" + RUNNING = "running" + FREEZE = "freeze" diff --git a/hassio/core.py b/supervisor/core.py similarity index 81% rename from hassio/core.py rename to supervisor/core.py index ae557ae60..dd2e78e17 100644 --- a/hassio/core.py +++ b/supervisor/core.py @@ -1,43 +1,47 @@ -"""Main file for Hass.io.""" -from contextlib import suppress +"""Main file for Supervisor.""" import asyncio +from contextlib import suppress import logging import async_timeout -from .coresys import CoreSysAttributes from .const import ( - STARTUP_SYSTEM, - STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE, + STARTUP_SERVICES, + STARTUP_SYSTEM, + CoreStates, ) +from .coresys import CoreSys, CoreSysAttributes from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError _LOGGER: logging.Logger = logging.getLogger(__name__) -class HassIO(CoreSysAttributes): - """Main object of Hass.io.""" +class Core(CoreSysAttributes): + """Main object of Supervisor.""" - def __init__(self, coresys): - """Initialize Hass.io object.""" - self.coresys = coresys + def __init__(self, coresys: CoreSys): + """Initialize Supervisor object.""" + self.coresys: CoreSys = coresys + self.state: CoreStates = CoreStates.INITIALIZE async def connect(self): """Connect Supervisor container.""" await self.sys_supervisor.load() async def setup(self): - """Setup HassIO orchestration.""" + """Setup supervisor orchestration.""" + self.state = CoreStates.STARTUP + # Load DBus await self.sys_dbus.load() # 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() @@ -76,7 +80,7 @@ class HassIO(CoreSysAttributes): await self.sys_secrets.load() async def start(self): - """Start Hass.io orchestration.""" + """Start Supervisor orchestration.""" await self.sys_api.start() # Mark booted partition as healthy @@ -87,7 +91,7 @@ class HassIO(CoreSysAttributes): if self.sys_supervisor.need_update: try: if self.sys_dev: - _LOGGER.warning("Ignore Hass.io updates on dev!") + _LOGGER.warning("Ignore Supervisor updates on dev!") else: await self.sys_supervisor.update() except SupervisorUpdateError: @@ -102,7 +106,7 @@ class HassIO(CoreSysAttributes): try: # HomeAssistant is already running / supervisor have only reboot if self.sys_hardware.last_boot == self.sys_config.last_boot: - _LOGGER.info("Hass.io reboot detected") + _LOGGER.info("Supervisor reboot detected") return # reset register services / discovery @@ -138,7 +142,12 @@ class HassIO(CoreSysAttributes): if self.sys_homeassistant.version == "landingpage": self.sys_create_task(self.sys_homeassistant.install()) - _LOGGER.info("Hass.io is up and running") + _LOGGER.info("Supervisor is up and running") + self.state = CoreStates.RUNNING + + # On full host boot, relaod information + self.sys_create_task(self.sys_host.reload()) + self.sys_create_task(self.sys_updater.reload()) async def stop(self): """Stop a running orchestration.""" @@ -146,7 +155,8 @@ class HassIO(CoreSysAttributes): self.sys_scheduler.suspend = True # store new last boot / prevent time adjustments - self._update_last_boot() + if self.state == CoreStates.RUNNING: + self._update_last_boot() # process async stop tasks try: @@ -163,7 +173,7 @@ class HassIO(CoreSysAttributes): except asyncio.TimeoutError: _LOGGER.warning("Force Shutdown!") - _LOGGER.info("Hass.io is down") + _LOGGER.info("Supervisor is down") async def shutdown(self): """Shutdown all running containers in correct order.""" @@ -184,7 +194,7 @@ class HassIO(CoreSysAttributes): async def repair(self): """Repair system integrity.""" - _LOGGER.info("Start repairing of Hass.io Environment") + _LOGGER.info("Start repairing of Supervisor Environment") await self.sys_run_in_executor(self.sys_docker.repair) # Restore core functionality @@ -198,4 +208,4 @@ class HassIO(CoreSysAttributes): # Tag version for latest await self.sys_supervisor.repair() - _LOGGER.info("Finished repairing of Hass.io Environment") + _LOGGER.info("Finished repairing of Supervisor Environment") diff --git a/hassio/coresys.py b/supervisor/coresys.py similarity index 93% rename from hassio/coresys.py rename to supervisor/coresys.py index 0e63d44b7..ecfb058f2 100644 --- a/hassio/coresys.py +++ b/supervisor/coresys.py @@ -15,8 +15,9 @@ 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 HassIO + from .core import Core from .dbus import DBusManager from .discovery import Discovery from .dns import CoreDNS @@ -39,7 +40,7 @@ class CoreSys: def __init__(self): """Initialize coresys.""" # Static attributes - self.machine_id: str = None + self.machine_id: Optional[str] = None # External objects self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop() @@ -55,8 +56,9 @@ class CoreSys: self._scheduler: Scheduler = Scheduler() # Internal objects pointers - self._core: Optional[HassIO] = None + 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 @@ -77,7 +79,7 @@ class CoreSys: @property def machine(self) -> str: - """Return running machine type of the Hass.io system.""" + """Return running machine type of the Supervisor system.""" if self._homeassistant: return self._homeassistant.machine return None @@ -128,15 +130,15 @@ class CoreSys: return self._scheduler @property - def core(self) -> HassIO: - """Return HassIO object.""" + def core(self) -> Core: + """Return core object.""" return self._core @core.setter - def core(self, value: HassIO): - """Set a Hass.io object.""" + def core(self, value: Core): + """Set a Core object.""" if self._core: - raise RuntimeError("Hass.io already set!") + raise RuntimeError("Core already set!") self._core = value @property @@ -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.""" @@ -363,7 +377,7 @@ class CoreSysAttributes: @property def sys_machine(self) -> str: - """Return running machine type of the Hass.io system.""" + """Return running machine type of the Supervisor system.""" return self.coresys.machine @property @@ -417,8 +431,8 @@ class CoreSysAttributes: return self.coresys.scheduler @property - def sys_core(self) -> HassIO: - """Return HassIO object.""" + def sys_core(self) -> Core: + """Return core object.""" return self.coresys.core @property @@ -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.""" diff --git a/hassio/data/arch.json b/supervisor/data/arch.json similarity index 100% rename from hassio/data/arch.json rename to supervisor/data/arch.json diff --git a/hassio/data/coredns.tmpl b/supervisor/data/coredns.tmpl similarity index 100% rename from hassio/data/coredns.tmpl rename to supervisor/data/coredns.tmpl diff --git a/hassio/data/hosts.tmpl b/supervisor/data/hosts.tmpl similarity index 100% rename from hassio/data/hosts.tmpl rename to supervisor/data/hosts.tmpl diff --git a/supervisor/data/pulse-client.tmpl b/supervisor/data/pulse-client.tmpl new file mode 100644 index 000000000..9488d23cd --- /dev/null +++ b/supervisor/data/pulse-client.tmpl @@ -0,0 +1,36 @@ +# 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 . + +## Configuration file for PulseAudio clients. See pulse-client.conf(5) for +## more information. Default values are commented out. Use either ; or # for +## commenting. + +{% if default_sink %}default-sink = {{ default_sink }}{% endif %} +{% if default_source %}default-source = {{ default_source }}{% endif %} + +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 diff --git a/hassio/dbus/__init__.py b/supervisor/dbus/__init__.py similarity index 100% rename from hassio/dbus/__init__.py rename to supervisor/dbus/__init__.py diff --git a/hassio/dbus/hostname.py b/supervisor/dbus/hostname.py similarity index 100% rename from hassio/dbus/hostname.py rename to supervisor/dbus/hostname.py diff --git a/hassio/dbus/interface.py b/supervisor/dbus/interface.py similarity index 100% rename from hassio/dbus/interface.py rename to supervisor/dbus/interface.py diff --git a/hassio/dbus/nmi_dns.py b/supervisor/dbus/nmi_dns.py similarity index 100% rename from hassio/dbus/nmi_dns.py rename to supervisor/dbus/nmi_dns.py diff --git a/hassio/dbus/rauc.py b/supervisor/dbus/rauc.py similarity index 100% rename from hassio/dbus/rauc.py rename to supervisor/dbus/rauc.py diff --git a/hassio/dbus/systemd.py b/supervisor/dbus/systemd.py similarity index 100% rename from hassio/dbus/systemd.py rename to supervisor/dbus/systemd.py diff --git a/hassio/dbus/utils.py b/supervisor/dbus/utils.py similarity index 100% rename from hassio/dbus/utils.py rename to supervisor/dbus/utils.py diff --git a/hassio/discovery/__init__.py b/supervisor/discovery/__init__.py similarity index 100% rename from hassio/discovery/__init__.py rename to supervisor/discovery/__init__.py diff --git a/hassio/discovery/const.py b/supervisor/discovery/const.py similarity index 100% rename from hassio/discovery/const.py rename to supervisor/discovery/const.py diff --git a/hassio/discovery/services/__init__.py b/supervisor/discovery/services/__init__.py similarity index 100% rename from hassio/discovery/services/__init__.py rename to supervisor/discovery/services/__init__.py diff --git a/hassio/discovery/services/adguard.py b/supervisor/discovery/services/adguard.py similarity index 82% rename from hassio/discovery/services/adguard.py rename to supervisor/discovery/services/adguard.py index 2b8610581..c840298c9 100644 --- a/hassio/discovery/services/adguard.py +++ b/supervisor/discovery/services/adguard.py @@ -1,7 +1,7 @@ """Discovery service for AdGuard.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ATTR_HOST, ATTR_PORT diff --git a/hassio/discovery/services/almond.py b/supervisor/discovery/services/almond.py similarity index 82% rename from hassio/discovery/services/almond.py rename to supervisor/discovery/services/almond.py index 81bcd1f0c..53b7cbd04 100644 --- a/hassio/discovery/services/almond.py +++ b/supervisor/discovery/services/almond.py @@ -1,7 +1,7 @@ """Discovery service for Almond.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ATTR_HOST, ATTR_PORT diff --git a/hassio/discovery/services/deconz.py b/supervisor/discovery/services/deconz.py similarity index 89% rename from hassio/discovery/services/deconz.py rename to supervisor/discovery/services/deconz.py index 63ac14bf9..a021a46d8 100644 --- a/hassio/discovery/services/deconz.py +++ b/supervisor/discovery/services/deconz.py @@ -1,7 +1,7 @@ """Discovery service for MQTT.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ATTR_HOST, ATTR_PORT, ATTR_API_KEY, ATTR_SERIAL diff --git a/hassio/discovery/services/home_panel.py b/supervisor/discovery/services/home_panel.py similarity index 82% rename from hassio/discovery/services/home_panel.py rename to supervisor/discovery/services/home_panel.py index 19e076167..147c2bdb4 100644 --- a/hassio/discovery/services/home_panel.py +++ b/supervisor/discovery/services/home_panel.py @@ -1,7 +1,7 @@ """Discovery service for Home Panel.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ATTR_HOST, ATTR_PORT diff --git a/hassio/discovery/services/mqtt.py b/supervisor/discovery/services/mqtt.py similarity index 93% rename from hassio/discovery/services/mqtt.py rename to supervisor/discovery/services/mqtt.py index 904d6cdab..20faf1710 100644 --- a/hassio/discovery/services/mqtt.py +++ b/supervisor/discovery/services/mqtt.py @@ -1,7 +1,7 @@ """Discovery service for MQTT.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ( ATTR_HOST, diff --git a/hassio/discovery/services/unifi.py b/supervisor/discovery/services/unifi.py similarity index 82% rename from hassio/discovery/services/unifi.py rename to supervisor/discovery/services/unifi.py index 473939a7e..072097889 100644 --- a/hassio/discovery/services/unifi.py +++ b/supervisor/discovery/services/unifi.py @@ -1,7 +1,7 @@ """Discovery service for UniFi.""" import voluptuous as vol -from hassio.validate import network_port +from supervisor.validate import network_port from ..const import ATTR_HOST, ATTR_PORT diff --git a/hassio/discovery/validate.py b/supervisor/discovery/validate.py similarity index 93% rename from hassio/discovery/validate.py rename to supervisor/discovery/validate.py index c2a1bc31d..612c85a04 100644 --- a/hassio/discovery/validate.py +++ b/supervisor/discovery/validate.py @@ -20,7 +20,7 @@ def valid_discovery_service(service): def valid_discovery_config(service, config): """Validate service name.""" try: - service_mod = import_module(f".services.{service}", "hassio.discovery") + service_mod = import_module(f".services.{service}", "supervisor.discovery") except ImportError: raise vol.Invalid(f"Service {service} not found") diff --git a/hassio/dns.py b/supervisor/dns.py similarity index 100% rename from hassio/dns.py rename to supervisor/dns.py diff --git a/hassio/docker/__init__.py b/supervisor/docker/__init__.py similarity index 98% rename from hassio/docker/__init__.py rename to supervisor/docker/__init__.py index 8274d506a..a517dd187 100644 --- a/hassio/docker/__init__.py +++ b/supervisor/docker/__init__.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Docker object.""" +"""Init file for Supervisor Docker object.""" from contextlib import suppress from ipaddress import IPv4Address import logging @@ -23,7 +23,7 @@ class CommandReturn: class DockerAPI: - """Docker Hass.io wrapper. + """Docker Supervisor wrapper. This class is not AsyncIO safe! """ diff --git a/hassio/docker/addon.py b/supervisor/docker/addon.py similarity index 95% rename from hassio/docker/addon.py rename to supervisor/docker/addon.py index d6d8c4e69..f1509e720 100644 --- a/hassio/docker/addon.py +++ b/supervisor/docker/addon.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-on Docker object.""" +"""Init file for Supervisor add-on Docker object.""" from __future__ import annotations from contextlib import suppress @@ -35,12 +35,11 @@ if TYPE_CHECKING: _LOGGER: logging.Logger = logging.getLogger(__name__) -AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm" NO_ADDDRESS = ip_address("0.0.0.0") class DockerAddon(DockerInterface): - """Docker Hass.io wrapper for Home Assistant.""" + """Docker Supervisor wrapper for Home Assistant.""" def __init__(self, coresys: CoreSys, addon: Addon): """Initialize Docker Home Assistant wrapper.""" @@ -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", + }, } ) diff --git a/supervisor/docker/audio.py b/supervisor/docker/audio.py new file mode 100644 index 000000000..bf9e62a90 --- /dev/null +++ b/supervisor/docker/audio.py @@ -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, + ) diff --git a/hassio/docker/dns.py b/supervisor/docker/dns.py similarity index 90% rename from hassio/docker/dns.py rename to supervisor/docker/dns.py index fd489c00a..258cc6796 100644 --- a/hassio/docker/dns.py +++ b/supervisor/docker/dns.py @@ -1,4 +1,4 @@ -"""HassOS Cli docker object.""" +"""DNS docker object.""" from contextlib import suppress import logging @@ -13,11 +13,11 @@ DNS_DOCKER_NAME: str = "hassio_dns" class DockerDNS(DockerInterface, CoreSysAttributes): - """Docker Hass.io wrapper for Hass.io DNS.""" + """Docker Supervisor wrapper for Supervisor DNS.""" @property def image(self) -> str: - """Return name of Hass.io DNS image.""" + """Return name of Supervisor DNS image.""" return f"homeassistant/{self.sys_arch.supervisor}-hassio-dns" @property @@ -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"} diff --git a/hassio/docker/hassos_cli.py b/supervisor/docker/hassos_cli.py similarity index 95% rename from hassio/docker/hassos_cli.py rename to supervisor/docker/hassos_cli.py index f9eaa6977..64b30c858 100644 --- a/hassio/docker/hassos_cli.py +++ b/supervisor/docker/hassos_cli.py @@ -10,7 +10,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class DockerHassOSCli(DockerInterface, CoreSysAttributes): - """Docker Hass.io wrapper for HassOS Cli.""" + """Docker Supervisor wrapper for HassOS Cli.""" @property def image(self): diff --git a/hassio/docker/homeassistant.py b/supervisor/docker/homeassistant.py similarity index 97% rename from hassio/docker/homeassistant.py rename to supervisor/docker/homeassistant.py index d011452af..7d7f5e24a 100644 --- a/hassio/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Docker object.""" +"""Init file for Supervisor Docker object.""" from contextlib import suppress from ipaddress import IPv4Address import logging @@ -16,7 +16,7 @@ HASS_DOCKER_NAME = "homeassistant" class DockerHomeAssistant(DockerInterface): - """Docker Hass.io wrapper for Home Assistant.""" + """Docker Supervisor wrapper for Home Assistant.""" @property def machine(self) -> Optional[str]: diff --git a/hassio/docker/interface.py b/supervisor/docker/interface.py similarity index 99% rename from hassio/docker/interface.py rename to supervisor/docker/interface.py index dddaf5524..ba0c5c54a 100644 --- a/hassio/docker/interface.py +++ b/supervisor/docker/interface.py @@ -1,4 +1,4 @@ -"""Interface class for Hass.io Docker object.""" +"""Interface class for Supervisor Docker object.""" import asyncio from contextlib import suppress import logging @@ -17,7 +17,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class DockerInterface(CoreSysAttributes): - """Docker Hass.io interface.""" + """Docker Supervisor interface.""" def __init__(self, coresys: CoreSys): """Initialize Docker base wrapper.""" diff --git a/hassio/docker/network.py b/supervisor/docker/network.py similarity index 88% rename from hassio/docker/network.py rename to supervisor/docker/network.py index 610a4db61..1a0fbcf4e 100644 --- a/hassio/docker/network.py +++ b/supervisor/docker/network.py @@ -1,4 +1,4 @@ -"""Internal network manager for Hass.io.""" +"""Internal network manager for Supervisor.""" from contextlib import suppress from ipaddress import IPv4Address import logging @@ -13,13 +13,13 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class DockerNetwork: - """Internal Hass.io Network. + """Internal Supervisor Network. This class is not AsyncIO safe! """ def __init__(self, docker_client: docker.DockerClient): - """Initialize internal Hass.io network.""" + """Initialize internal Supervisor network.""" self.docker: docker.DockerClient = docker_client self.network: docker.models.networks.Network = self._get_network() @@ -48,12 +48,17 @@ 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 HassIO network.""" + """Get supervisor network.""" try: return self.docker.networks.get(DOCKER_NETWORK) except docker.errors.NotFound: - _LOGGER.info("Can't find Hass.io network, create new network") + _LOGGER.info("Can't find Supervisor network, create new network") ipam_pool = docker.types.IPAMPool( subnet=str(DOCKER_NETWORK_MASK), @@ -77,7 +82,7 @@ class DockerNetwork: alias: Optional[List[str]] = None, ipv4: Optional[IPv4Address] = None, ) -> None: - """Attach container to Hass.io network. + """Attach container to Supervisor network. Need run inside executor. """ diff --git a/hassio/docker/stats.py b/supervisor/docker/stats.py similarity index 100% rename from hassio/docker/stats.py rename to supervisor/docker/stats.py diff --git a/hassio/docker/supervisor.py b/supervisor/docker/supervisor.py similarity index 88% rename from hassio/docker/supervisor.py rename to supervisor/docker/supervisor.py index 174067b23..d7c9f5f04 100644 --- a/hassio/docker/supervisor.py +++ b/supervisor/docker/supervisor.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io Docker object.""" +"""Init file for Supervisor Docker object.""" from ipaddress import IPv4Address import logging import os @@ -14,7 +14,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class DockerSupervisor(DockerInterface, CoreSysAttributes): - """Docker Hass.io wrapper for Supervisor.""" + """Docker Supervisor wrapper for Supervisor.""" @property def name(self) -> str: @@ -53,9 +53,11 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes): return # Attach to network - _LOGGER.info("Connect Supervisor to Hass.io Network") + _LOGGER.info("Connect Supervisor to hassio Network") self.sys_docker.network.attach_container( - docker_container, alias=["hassio"], ipv4=self.sys_docker.network.supervisor + docker_container, + alias=["supervisor"], + ipv4=self.sys_docker.network.supervisor, ) def retag(self) -> Awaitable[None]: diff --git a/hassio/exceptions.py b/supervisor/exceptions.py similarity index 92% rename from hassio/exceptions.py rename to supervisor/exceptions.py index aa14db239..30b35c11e 100644 --- a/hassio/exceptions.py +++ b/supervisor/exceptions.py @@ -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 @@ -199,3 +210,10 @@ class DockerAPIError(HassioError): class HardwareNotSupportedError(HassioNotSupportedError): """Raise if hardware function is not supported.""" + + +# Pulse Audio + + +class PulseAudioError(HassioError): + """Raise if an sound error is happening.""" diff --git a/hassio/hassos.py b/supervisor/hassos.py similarity index 98% rename from hassio/hassos.py rename to supervisor/hassos.py index 1e3b110c5..a7f745251 100644 --- a/hassio/hassos.py +++ b/supervisor/hassos.py @@ -23,7 +23,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class HassOS(CoreSysAttributes): - """HassOS interface inside HassIO.""" + """HassOS interface inside supervisor.""" def __init__(self, coresys: CoreSys): """Initialize HassOS handler.""" @@ -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: diff --git a/hassio/homeassistant.py b/supervisor/homeassistant.py similarity index 99% rename from hassio/homeassistant.py rename to supervisor/homeassistant.py index 463dcc4ff..b10e3e0ea 100644 --- a/hassio/homeassistant.py +++ b/supervisor/homeassistant.py @@ -219,7 +219,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): @property def hassio_token(self) -> str: - """Return an access token for the Hass.io API.""" + """Return an access token for the Supervisor API.""" return self._data.get(ATTR_ACCESS_TOKEN) @property diff --git a/hassio/host/__init__.py b/supervisor/host/__init__.py similarity index 87% rename from hassio/host/__init__.py rename to supervisor/host/__init__.py index 8fe2b5404..c88ec8598 100644 --- a/hassio/host/__init__.py +++ b/supervisor/host/__init__.py @@ -2,21 +2,21 @@ from contextlib import suppress import logging -from .alsa import AlsaAudio +from ..const import ( + FEATURES_HASSOS, + FEATURES_HOSTNAME, + FEATURES_REBOOT, + FEATURES_SERVICES, + FEATURES_SHUTDOWN, +) +from ..coresys import CoreSys, CoreSysAttributes +from ..exceptions import HassioError, PulseAudioError from .apparmor import AppArmorControl from .control import SystemControl from .info import InfoCenter -from .services import ServiceManager from .network import NetworkManager -from ..const import ( - FEATURES_REBOOT, - FEATURES_SHUTDOWN, - FEATURES_HOSTNAME, - FEATURES_SERVICES, - FEATURES_HASSOS, -) -from ..coresys import CoreSysAttributes, CoreSys -from ..exceptions import HassioError +from .services import ServiceManager +from .sound import SoundControl _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -28,17 +28,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 + self._sound: SoundControl = SoundControl(coresys) @property def apparmor(self) -> AppArmorControl: @@ -65,6 +60,11 @@ class HostManager(CoreSysAttributes): """Return host NetworkManager handler.""" return self._network + @property + def sound(self) -> SoundControl: + """Return host PulseAudio control.""" + return self._sound + @property def supperted_features(self): """Return a list of supported host features.""" @@ -92,6 +92,9 @@ class HostManager(CoreSysAttributes): if self.sys_dbus.nmi_dns.is_connected: await self.network.update() + with suppress(PulseAudioError): + await self.sound.update() + async def load(self): """Load host information.""" with suppress(HassioError): diff --git a/hassio/host/apparmor.py b/supervisor/host/apparmor.py similarity index 100% rename from hassio/host/apparmor.py rename to supervisor/host/apparmor.py diff --git a/hassio/host/control.py b/supervisor/host/control.py similarity index 100% rename from hassio/host/control.py rename to supervisor/host/control.py diff --git a/hassio/host/info.py b/supervisor/host/info.py similarity index 100% rename from hassio/host/info.py rename to supervisor/host/info.py diff --git a/hassio/host/network.py b/supervisor/host/network.py similarity index 100% rename from hassio/host/network.py rename to supervisor/host/network.py diff --git a/hassio/host/services.py b/supervisor/host/services.py similarity index 100% rename from hassio/host/services.py rename to supervisor/host/services.py diff --git a/supervisor/host/sound.py b/supervisor/host/sound.py new file mode 100644 index 000000000..5d97ce3df --- /dev/null +++ b/supervisor/host/sound.py @@ -0,0 +1,131 @@ +"""Pulse host control.""" +from enum import Enum +import logging +from typing import List + +import attr +from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed + +from ..coresys import CoreSys, CoreSysAttributes +from ..exceptions import PulseAudioError + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +PULSE_NAME = "supervisor" + + +class SourceType(str, Enum): + """INPUT/OUTPUT type of source.""" + + INPUT = "input" + OUTPUT = "output" + + +@attr.s(frozen=True) +class AudioProfile: + """Represent a input/output profile.""" + + name: str = attr.ib() + description: str = attr.ib() + volume: float = attr.ib() + default: bool = attr.ib() + + +class SoundControl(CoreSysAttributes): + """Pulse control from Host.""" + + def __init__(self, coresys: CoreSys) -> None: + """Initialize PulseAudio sound control.""" + self.coresys: CoreSys = coresys + self._input: List[AudioProfile] = [] + self._output: List[AudioProfile] = [] + + @property + def input_profiles(self) -> List[AudioProfile]: + """Return a list of available input profiles.""" + return self._input + + @property + def output_profiles(self) -> List[AudioProfile]: + """Return a list of available output profiles.""" + return self._output + + async def set_default(self, source: SourceType, name: str) -> None: + """Set a profile to default input/output.""" + try: + with Pulse(PULSE_NAME) as pulse: + if source == SourceType.OUTPUT: + # Get source and set it as default + source = pulse.get_source_by_name(name) + pulse.source_default_set(source) + else: + # Get sink and set it as default + sink = pulse.get_sink_by_name(name) + pulse.sink_default_set(sink) + except PulseIndexError: + _LOGGER.error("Can't find %s profile %s", source, name) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.error("Can't set %s as default: %s", name, err) + raise PulseAudioError() from None + + # Reload data + await self.update() + + async def set_volume(self, source: SourceType, name: str, volume: float) -> None: + """Set a profile to volume input/output.""" + try: + with Pulse(PULSE_NAME) as pulse: + if source == SourceType.OUTPUT: + # Get source and set it as default + source = pulse.get_source_by_name(name) + else: + # Get sink and set it as default + source = pulse.get_sink_by_name(name) + + pulse.volume_set_all_chans(source, volume) + except PulseIndexError: + _LOGGER.error("Can't find %s profile %s", source, name) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.error("Can't set %s volume: %s", name, err) + raise PulseAudioError() from None + + # Reload data + await self.update() + + async def update(self): + """Update properties over dbus.""" + _LOGGER.info("Update PulseAudio information") + try: + with Pulse(PULSE_NAME) as pulse: + server = pulse.server_info() + + # Update output + self._output.clear() + for sink in pulse.sink_list(): + self._output.append( + AudioProfile( + sink.name, + sink.description, + sink.volume.value_flat, + sink.name == server.default_sink_name, + ) + ) + + # Update input + self._input.clear() + for source in pulse.source_list(): + self._input.append( + AudioProfile( + source.name, + source.description, + source.volume.value_flat, + source.name == server.default_source_name, + ) + ) + except PulseOperationFailed as err: + _LOGGER.error("Error while processing pulse update: %s", err) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.debug("Can't update PulseAudio data: %s", err) diff --git a/hassio/ingress.py b/supervisor/ingress.py similarity index 100% rename from hassio/ingress.py rename to supervisor/ingress.py diff --git a/supervisor/misc/__init__.py b/supervisor/misc/__init__.py new file mode 100644 index 000000000..0144cbd3b --- /dev/null +++ b/supervisor/misc/__init__.py @@ -0,0 +1 @@ +"""Special object and tools for Supervisor.""" diff --git a/hassio/misc/forwarder.py b/supervisor/misc/forwarder.py similarity index 100% rename from hassio/misc/forwarder.py rename to supervisor/misc/forwarder.py diff --git a/hassio/misc/hardware.py b/supervisor/misc/hardware.py similarity index 97% rename from hassio/misc/hardware.py rename to supervisor/misc/hardware.py index 9dda6510a..7d6919c6d 100644 --- a/hassio/misc/hardware.py +++ b/supervisor/misc/hardware.py @@ -28,7 +28,7 @@ GPIO_DEVICES: Path = Path("/sys/class/gpio") SOC_DEVICES: Path = Path("/sys/devices/platform/soc") RE_TTY: re.Pattern = re.compile(r"tty[A-Z]+") -RE_VIDEO_DEVICES = re.compile(r"^(?:vchiq|cec|video\d+)") +RE_VIDEO_DEVICES = re.compile(r"^(?:vchiq|cec\d+|video\d+)") @attr.s(frozen=True) @@ -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: diff --git a/hassio/misc/scheduler.py b/supervisor/misc/scheduler.py similarity index 96% rename from hassio/misc/scheduler.py rename to supervisor/misc/scheduler.py index 3e71fc040..655a8c31b 100644 --- a/hassio/misc/scheduler.py +++ b/supervisor/misc/scheduler.py @@ -1,4 +1,4 @@ -"""Schedule for Hass.io.""" +"""Schedule for Supervisor.""" import asyncio from datetime import date, datetime, time, timedelta import logging @@ -12,7 +12,7 @@ TASK = "task" class Scheduler: - """Schedule task inside Hass.io.""" + """Schedule task inside Supervisor.""" def __init__(self): """Initialize task schedule.""" diff --git a/hassio/secrets.py b/supervisor/secrets.py similarity index 100% rename from hassio/secrets.py rename to supervisor/secrets.py diff --git a/hassio/services/__init__.py b/supervisor/services/__init__.py similarity index 100% rename from hassio/services/__init__.py rename to supervisor/services/__init__.py diff --git a/hassio/services/const.py b/supervisor/services/const.py similarity index 100% rename from hassio/services/const.py rename to supervisor/services/const.py diff --git a/hassio/services/data.py b/supervisor/services/data.py similarity index 100% rename from hassio/services/data.py rename to supervisor/services/data.py diff --git a/hassio/services/interface.py b/supervisor/services/interface.py similarity index 100% rename from hassio/services/interface.py rename to supervisor/services/interface.py diff --git a/hassio/services/modules/__init__.py b/supervisor/services/modules/__init__.py similarity index 100% rename from hassio/services/modules/__init__.py rename to supervisor/services/modules/__init__.py diff --git a/hassio/services/modules/mqtt.py b/supervisor/services/modules/mqtt.py similarity index 94% rename from hassio/services/modules/mqtt.py rename to supervisor/services/modules/mqtt.py index eec10603f..29c1776fe 100644 --- a/hassio/services/modules/mqtt.py +++ b/supervisor/services/modules/mqtt.py @@ -2,9 +2,9 @@ import logging from typing import Any, Dict, List -from hassio.addons.addon import Addon -from hassio.exceptions import ServicesError -from hassio.validate import network_port +from supervisor.addons.addon import Addon +from supervisor.exceptions import ServicesError +from supervisor.validate import network_port import voluptuous as vol from ..const import ( diff --git a/hassio/services/modules/mysql.py b/supervisor/services/modules/mysql.py similarity index 94% rename from hassio/services/modules/mysql.py rename to supervisor/services/modules/mysql.py index 368646416..fb80d639d 100644 --- a/hassio/services/modules/mysql.py +++ b/supervisor/services/modules/mysql.py @@ -2,9 +2,9 @@ import logging from typing import Any, Dict, List -from hassio.addons.addon import Addon -from hassio.exceptions import ServicesError -from hassio.validate import network_port +from supervisor.addons.addon import Addon +from supervisor.exceptions import ServicesError +from supervisor.validate import network_port import voluptuous as vol from ..const import ( diff --git a/hassio/services/validate.py b/supervisor/services/validate.py similarity index 100% rename from hassio/services/validate.py rename to supervisor/services/validate.py diff --git a/hassio/snapshots/__init__.py b/supervisor/snapshots/__init__.py similarity index 100% rename from hassio/snapshots/__init__.py rename to supervisor/snapshots/__init__.py diff --git a/hassio/snapshots/snapshot.py b/supervisor/snapshots/snapshot.py similarity index 99% rename from hassio/snapshots/snapshot.py rename to supervisor/snapshots/snapshot.py index 0881485d9..8b2084707 100644 --- a/hassio/snapshots/snapshot.py +++ b/supervisor/snapshots/snapshot.py @@ -59,7 +59,7 @@ MAP_FOLDER_EXCLUDE = { class Snapshot(CoreSysAttributes): - """A single Hass.io snapshot.""" + """A single Supervisor snapshot.""" def __init__(self, coresys: CoreSys, tar_file: Path): """Initialize a snapshot.""" @@ -351,7 +351,7 @@ class Snapshot(CoreSysAttributes): await asyncio.wait(tasks) async def store_folders(self, folder_list=None): - """Backup Hass.io data into snapshot.""" + """Backup Supervisor data into snapshot.""" folder_list = set(folder_list or ALL_FOLDERS) def _folder_save(name): @@ -388,7 +388,7 @@ class Snapshot(CoreSysAttributes): await asyncio.wait(tasks) async def restore_folders(self, folder_list=None): - """Backup Hass.io data into snapshot.""" + """Backup Supervisor data into snapshot.""" folder_list = set(folder_list or self.folders) def _folder_restore(name): diff --git a/hassio/snapshots/utils.py b/supervisor/snapshots/utils.py similarity index 100% rename from hassio/snapshots/utils.py rename to supervisor/snapshots/utils.py diff --git a/hassio/snapshots/validate.py b/supervisor/snapshots/validate.py similarity index 100% rename from hassio/snapshots/validate.py rename to supervisor/snapshots/validate.py diff --git a/hassio/store/__init__.py b/supervisor/store/__init__.py similarity index 97% rename from hassio/store/__init__.py rename to supervisor/store/__init__.py index b9bf8cfb1..025edd86d 100644 --- a/hassio/store/__init__.py +++ b/supervisor/store/__init__.py @@ -15,7 +15,7 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL)) class StoreManager(CoreSysAttributes): - """Manage add-ons inside Hass.io.""" + """Manage add-ons inside Supervisor.""" def __init__(self, coresys: CoreSys): """Initialize Docker base wrapper.""" @@ -32,7 +32,7 @@ class StoreManager(CoreSysAttributes): """Start up add-on management.""" self.data.update() - # Init Hass.io built-in repositories + # Init Supervisor built-in repositories repositories = set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES # Init custom repositories and load add-ons diff --git a/hassio/store/addon.py b/supervisor/store/addon.py similarity index 88% rename from hassio/store/addon.py rename to supervisor/store/addon.py index 3dfcf5cd2..fca2bf291 100644 --- a/hassio/store/addon.py +++ b/supervisor/store/addon.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-ons.""" +"""Init file for Supervisor add-ons.""" import logging from ..coresys import CoreSys @@ -8,7 +8,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class AddonStore(AddonModel): - """Hold data for add-on inside Hass.io.""" + """Hold data for add-on inside Supervisor.""" def __init__(self, coresys: CoreSys, slug: str): """Initialize data holder.""" diff --git a/hassio/store/built-in.json b/supervisor/store/built-in.json similarity index 100% rename from hassio/store/built-in.json rename to supervisor/store/built-in.json diff --git a/hassio/store/data.py b/supervisor/store/data.py similarity index 97% rename from hassio/store/data.py rename to supervisor/store/data.py index 8bb89f343..ef0f60aaf 100644 --- a/hassio/store/data.py +++ b/supervisor/store/data.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-on data.""" +"""Init file for Supervisor add-on data.""" import logging from pathlib import Path from typing import Any, Dict @@ -24,7 +24,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class StoreData(CoreSysAttributes): - """Hold data for Add-ons inside Hass.io.""" + """Hold data for Add-ons inside Supervisor.""" def __init__(self, coresys: CoreSys): """Initialize data holder.""" diff --git a/hassio/store/git.py b/supervisor/store/git.py similarity index 95% rename from hassio/store/git.py rename to supervisor/store/git.py index a60799230..9a2cce642 100644 --- a/hassio/store/git.py +++ b/supervisor/store/git.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io add-on Git.""" +"""Init file for Supervisor add-on Git.""" import asyncio import logging import functools as ft @@ -141,10 +141,10 @@ class GitRepo(CoreSysAttributes): class GitRepoHassIO(GitRepo): - """Hass.io add-ons repository.""" + """Supervisor add-ons repository.""" def __init__(self, coresys): - """Initialize Git Hass.io add-on repository.""" + """Initialize Git Supervisor add-on repository.""" super().__init__(coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS) @@ -152,7 +152,7 @@ class GitRepoCustom(GitRepo): """Custom add-ons repository.""" def __init__(self, coresys, url): - """Initialize custom Git Hass.io addo-n repository.""" + """Initialize custom Git Supervisor addo-n repository.""" path = Path(coresys.config.path_addons_git, get_hash_from_repository(url)) super().__init__(coresys, path, url) diff --git a/hassio/store/repository.py b/supervisor/store/repository.py similarity index 96% rename from hassio/store/repository.py rename to supervisor/store/repository.py index 690b16683..12624cf64 100644 --- a/hassio/store/repository.py +++ b/supervisor/store/repository.py @@ -1,4 +1,4 @@ -"""Represent a Hass.io repository.""" +"""Represent a Supervisor repository.""" from .git import GitRepoHassIO, GitRepoCustom from .utils import get_hash_from_repository from ..const import ( @@ -15,7 +15,7 @@ UNKNOWN = "unknown" class Repository(CoreSysAttributes): - """Repository in Hass.io.""" + """Repository in Supervisor.""" slug: str = None diff --git a/hassio/store/utils.py b/supervisor/store/utils.py similarity index 100% rename from hassio/store/utils.py rename to supervisor/store/utils.py diff --git a/hassio/store/validate.py b/supervisor/store/validate.py similarity index 100% rename from hassio/store/validate.py rename to supervisor/store/validate.py diff --git a/hassio/supervisor.py b/supervisor/supervisor.py similarity index 95% rename from hassio/supervisor.py rename to supervisor/supervisor.py index 372af6fd6..0f4666649 100644 --- a/hassio/supervisor.py +++ b/supervisor/supervisor.py @@ -9,7 +9,7 @@ from typing import Awaitable, Optional import aiohttp -from .const import URL_HASSIO_APPARMOR, HASSIO_VERSION +from .const import URL_HASSIO_APPARMOR, SUPERVISOR_VERSION from .coresys import CoreSys, CoreSysAttributes from .docker.stats import DockerStats from .docker.supervisor import DockerSupervisor @@ -60,7 +60,7 @@ class Supervisor(CoreSysAttributes): @property def version(self) -> str: """Return version of running Home Assistant.""" - return HASSIO_VERSION + return SUPERVISOR_VERSION @property def latest_version(self) -> str: @@ -74,7 +74,7 @@ class Supervisor(CoreSysAttributes): @property def arch(self) -> str: - """Return arch of the Hass.io container.""" + """Return arch of the Supervisor container.""" return self.instance.arch async def update_apparmor(self) -> None: @@ -117,7 +117,7 @@ class Supervisor(CoreSysAttributes): try: await self.instance.update(version, latest=True) except DockerAPIError: - _LOGGER.error("Update of Hass.io fails!") + _LOGGER.error("Update of Supervisor fails!") raise SupervisorUpdateError() from None with suppress(SupervisorError): diff --git a/hassio/tasks.py b/supervisor/tasks.py similarity index 82% rename from hassio/tasks.py rename to supervisor/tasks.py index 2b2bc322d..e39d4afe9 100644 --- a/hassio/tasks.py +++ b/supervisor/tasks.py @@ -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,10 +25,11 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15 RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_DNS_DOCKER = 20 +RUN_WATCHDOG_AUDIO_DOCKER = 20 class Tasks(CoreSysAttributes): - """Handle Tasks inside Hass.io.""" + """Handle Tasks inside Supervisor.""" def __init__(self, coresys): """Initialize Tasks.""" @@ -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") @@ -119,16 +127,16 @@ class Tasks(CoreSysAttributes): await asyncio.wait(tasks) async def _update_supervisor(self): - """Check and run update of Supervisor Hass.io.""" + """Check and run update of Supervisor Supervisor.""" if not self.sys_supervisor.need_update: return # don't perform an update on dev channel if self.sys_dev: - _LOGGER.warning("Ignore Hass.io update on dev channel!") + _LOGGER.warning("Ignore Supervisor update on dev channel!") return - _LOGGER.info("Found new Hass.io version") + _LOGGER.info("Found new Supervisor version") await self.sys_supervisor.update() async def _watchdog_homeassistant_docker(self): @@ -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!") diff --git a/hassio/updater.py b/supervisor/updater.py similarity index 78% rename from hassio/updater.py rename to supervisor/updater.py index 354d1c8c7..47ac77001 100644 --- a/hassio/updater.py +++ b/supervisor/updater.py @@ -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, @@ -53,7 +54,7 @@ class Updater(JsonConfig, CoreSysAttributes): @property def version_hassio(self) -> Optional[str]: - """Return latest version of Hass.io.""" + """Return latest version of Supervisor.""" return self._data.get(ATTR_HASSIO) @property @@ -62,18 +63,23 @@ 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 Hass.io 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 Hass.io instance.""" + """Return upstream channel of Supervisor instance.""" return self._data[ATTR_CHANNEL] @channel.setter @@ -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. @@ -89,7 +95,6 @@ class Updater(JsonConfig, CoreSysAttributes): """ url = URL_HASSIO_VERSION.format(channel=self.channel) machine = self.sys_machine or "default" - board = self.sys_hassos.board try: _LOGGER.info("Fetch update data from %s", url) @@ -110,17 +115,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 - if self.sys_hassos.available and board: - self._data[ATTR_HASSOS] = data["hassos"][board] - self._data[ATTR_HASSOS_CLI] = data["hassos-cli"] + # Update HassOS version + if self.sys_hassos.board: + self._data[ATTR_HASSOS] = data["hassos"][self.sys_hassos.board] + + # 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) diff --git a/hassio/utils/__init__.py b/supervisor/utils/__init__.py similarity index 75% rename from hassio/utils/__init__.py rename to supervisor/utils/__init__.py index 4ff19cb18..f3c0d6349 100644 --- a/hassio/utils/__init__.py +++ b/supervisor/utils/__init__.py @@ -1,9 +1,11 @@ -"""Tools file for Hass.io.""" +"""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 diff --git a/hassio/utils/apparmor.py b/supervisor/utils/apparmor.py similarity index 100% rename from hassio/utils/apparmor.py rename to supervisor/utils/apparmor.py diff --git a/hassio/utils/dt.py b/supervisor/utils/dt.py similarity index 98% rename from hassio/utils/dt.py rename to supervisor/utils/dt.py index 972e77527..cfca9d760 100644 --- a/hassio/utils/dt.py +++ b/supervisor/utils/dt.py @@ -1,4 +1,4 @@ -"""Tools file for Hass.io.""" +"""Tools file for Supervisor.""" import asyncio from datetime import datetime, timedelta, timezone, tzinfo import logging diff --git a/hassio/utils/gdbus.py b/supervisor/utils/gdbus.py similarity index 100% rename from hassio/utils/gdbus.py rename to supervisor/utils/gdbus.py diff --git a/hassio/utils/json.py b/supervisor/utils/json.py similarity index 98% rename from hassio/utils/json.py rename to supervisor/utils/json.py index 044831b94..ecaa6fb7b 100644 --- a/hassio/utils/json.py +++ b/supervisor/utils/json.py @@ -1,4 +1,4 @@ -"""Tools file for Hass.io.""" +"""Tools file for Supervisor.""" import json import logging from pathlib import Path diff --git a/hassio/utils/tar.py b/supervisor/utils/tar.py similarity index 100% rename from hassio/utils/tar.py rename to supervisor/utils/tar.py diff --git a/hassio/utils/validate.py b/supervisor/utils/validate.py similarity index 100% rename from hassio/utils/validate.py rename to supervisor/utils/validate.py diff --git a/hassio/validate.py b/supervisor/validate.py similarity index 95% rename from hassio/validate.py rename to supervisor/validate.py index 785b6fff9..766a32d5e 100644 --- a/hassio/validate.py +++ b/supervisor/validate.py @@ -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[^#]+)(?:#(?P[\w\-]+))?$") # pylint: disable=no-value-for-parameter @@ -44,7 +44,6 @@ RE_REPOSITORY = re.compile(r"^(?P[^#]+)(?:#(?P[\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, +) diff --git a/tests/__init__.py b/tests/__init__.py index 4a9f81fe8..37b7f2b73 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Hass.io Testframework.""" +"""Supervisor Testframework.""" diff --git a/tests/addons/test_config.py b/tests/addons/test_config.py index f47ebc187..42c2d7d56 100644 --- a/tests/addons/test_config.py +++ b/tests/addons/test_config.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.addons import validate as vd +from supervisor.addons import validate as vd from ..common import load_json_fixture diff --git a/tests/addons/test_ui_schema.py b/tests/addons/test_ui_schema.py index bc73050d2..c61e429bc 100644 --- a/tests/addons/test_ui_schema.py +++ b/tests/addons/test_ui_schema.py @@ -1,6 +1,6 @@ """Test add-ons schema to UI schema convertion.""" -from hassio.addons.validate import schema_ui_options +from supervisor.addons.validate import schema_ui_options def test_simple_schema(): diff --git a/tests/conftest.py b/tests/conftest.py index ac105e071..72909e40e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from unittest.mock import patch, PropertyMock, MagicMock import pytest -from hassio.bootstrap import initialize_coresys +from supervisor.bootstrap import initialize_coresys from tests.common import mock_coro @@ -13,15 +13,15 @@ from tests.common import mock_coro @pytest.fixture def docker(): """Mock Docker API.""" - with patch("hassio.coresys.DockerAPI") as mock: + with patch("supervisor.coresys.DockerAPI") as mock: yield mock @pytest.fixture async def coresys(loop, docker): """Create a CoreSys Mock.""" - with patch("hassio.bootstrap.initialize_system_data"), patch( - "hassio.bootstrap.fetch_timezone", + with patch("supervisor.bootstrap.initialize_system_data"), patch( + "supervisor.bootstrap.fetch_timezone", return_value=mock_coro(return_value="Europe/Zurich"), ): coresys_obj = await initialize_coresys() @@ -34,12 +34,14 @@ async def coresys(loop, docker): @pytest.fixture def sys_machine(): """Mock sys_machine.""" - with patch("hassio.coresys.CoreSys.machine", new_callable=PropertyMock) as mock: + with patch("supervisor.coresys.CoreSys.machine", new_callable=PropertyMock) as mock: yield mock @pytest.fixture def sys_supervisor(): - with patch("hassio.coresys.CoreSys.supervisor", new_callable=PropertyMock) as mock: + with patch( + "supervisor.coresys.CoreSys.supervisor", new_callable=PropertyMock + ) as mock: mock.return_value = MagicMock() yield MagicMock diff --git a/tests/discovery/test_adguard.py b/tests/discovery/test_adguard.py index 9f54a1961..cc2a6b05e 100644 --- a/tests/discovery/test_adguard.py +++ b/tests/discovery/test_adguard.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_almond.py b/tests/discovery/test_almond.py index 78a8e186c..5a4548493 100644 --- a/tests/discovery/test_almond.py +++ b/tests/discovery/test_almond.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_deconz.py b/tests/discovery/test_deconz.py index 4f76db326..6d15c7815 100644 --- a/tests/discovery/test_deconz.py +++ b/tests/discovery/test_deconz.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_home_panel.py b/tests/discovery/test_home_panel.py index 883aeb678..cb768751c 100644 --- a/tests/discovery/test_home_panel.py +++ b/tests/discovery/test_home_panel.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_mqtt.py b/tests/discovery/test_mqtt.py index 06fa9a39a..0ffc55424 100644 --- a/tests/discovery/test_mqtt.py +++ b/tests/discovery/test_mqtt.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_unifi.py b/tests/discovery/test_unifi.py index 53dc9e4d4..848474641 100644 --- a/tests/discovery/test_unifi.py +++ b/tests/discovery/test_unifi.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery.validate import valid_discovery_config +from supervisor.discovery.validate import valid_discovery_config def test_good_config(): diff --git a/tests/discovery/test_validate.py b/tests/discovery/test_validate.py index 0aa6f7938..e619ecfb0 100644 --- a/tests/discovery/test_validate.py +++ b/tests/discovery/test_validate.py @@ -3,7 +3,7 @@ import voluptuous as vol import pytest -from hassio.discovery import validate +from supervisor.discovery import validate def test_valid_services(): diff --git a/tests/misc/test_hardware.py b/tests/misc/test_hardware.py index d969e7152..edc52e855 100644 --- a/tests/misc/test_hardware.py +++ b/tests/misc/test_hardware.py @@ -1,6 +1,8 @@ """Test hardware utils.""" +from unittest.mock import patch, PropertyMock +from pathlib import Path -from hassio.misc.hardware import Hardware +from supervisor.misc.hardware import Hardware, Device def test_read_all_devices(): @@ -8,3 +10,25 @@ def test_read_all_devices(): system = Hardware() assert system.devices + + +def test_video_devices(): + """Test video device filter.""" + system = Hardware() + device_list = [ + Device("test-dev", Path("/dev/test-dev"), []), + Device("vchiq", Path("/dev/vchiq"), []), + Device("cec0", Path("/dev/cec0"), []), + Device("video1", Path("/dev/video1"), []), + ] + + with patch( + "supervisor.misc.hardware.Hardware.devices", new_callable=PropertyMock + ) as mock_device: + mock_device.return_value = device_list + + assert system.video_devices == [ + Device("vchiq", Path("/dev/vchiq"), []), + Device("cec0", Path("/dev/cec0"), []), + Device("video1", Path("/dev/video1"), []), + ] diff --git a/tests/test_ingress.py b/tests/test_ingress.py index a1aaf570e..809545f88 100644 --- a/tests/test_ingress.py +++ b/tests/test_ingress.py @@ -1,7 +1,7 @@ """Test ingress.""" from datetime import timedelta -from hassio.utils.dt import utc_from_timestamp +from supervisor.utils.dt import utc_from_timestamp def test_session_handling(coresys): diff --git a/tests/test_validate.py b/tests/test_validate.py index 2f8a5d300..53ae4ec74 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,6 +1,6 @@ """Test validators.""" -import hassio.validate +import supervisor.validate import voluptuous.error import pytest @@ -20,42 +20,42 @@ BAD = ["hello world", "https://foo.bar", "", "dns://example.com"] async def test_dns_url_v4_good(): """ tests the DNS validator with known-good ipv6 DNS URLs """ for url in GOOD_V4: - assert hassio.validate.dns_url(url) + assert supervisor.validate.dns_url(url) async def test_dns_url_v6_good(): """ tests the DNS validator with known-good ipv6 DNS URLs """ for url in GOOD_V6: - assert hassio.validate.dns_url(url) + assert supervisor.validate.dns_url(url) async def test_dns_server_list_v4(): """ test a list with v4 addresses """ - assert hassio.validate.dns_server_list(GOOD_V4) + assert supervisor.validate.dns_server_list(GOOD_V4) async def test_dns_server_list_v6(): """ test a list with v6 addresses """ - assert hassio.validate.dns_server_list(GOOD_V6) + assert supervisor.validate.dns_server_list(GOOD_V6) async def test_dns_server_list_combined(): """ test a list with both v4 and v6 addresses """ combined = GOOD_V4 + GOOD_V6 # test the matches - assert hassio.validate.dns_server_list(combined) + assert supervisor.validate.dns_server_list(combined) # test max_length is OK still - assert hassio.validate.dns_server_list(combined) + assert supervisor.validate.dns_server_list(combined) # test that it fails when the list is too long with pytest.raises(voluptuous.error.Invalid): - hassio.validate.dns_server_list(combined + combined + combined + combined) + supervisor.validate.dns_server_list(combined + combined + combined + combined) async def test_dns_server_list_bad(): """ test the bad list """ # test the matches with pytest.raises(voluptuous.error.Invalid): - assert hassio.validate.dns_server_list(BAD) + assert supervisor.validate.dns_server_list(BAD) async def test_dns_server_list_bad_combined(): @@ -64,4 +64,4 @@ async def test_dns_server_list_bad_combined(): with pytest.raises(voluptuous.error.Invalid): # bad list - assert hassio.validate.dns_server_list(combined) + assert supervisor.validate.dns_server_list(combined) diff --git a/tests/utils/test_check_port.py b/tests/utils/test_check_port.py index f38e9c94d..63f3ca4b1 100644 --- a/tests/utils/test_check_port.py +++ b/tests/utils/test_check_port.py @@ -1,7 +1,7 @@ """Check ports.""" from ipaddress import ip_address -from hassio.utils import check_port +from supervisor.utils import check_port def test_exists_open_port(): diff --git a/tests/utils/test_gvariant_parser.py b/tests/utils/test_gvariant_parser.py index abd79f511..50aabef7d 100644 --- a/tests/utils/test_gvariant_parser.py +++ b/tests/utils/test_gvariant_parser.py @@ -1,5 +1,5 @@ """Test gdbus gvariant parser.""" -from hassio.utils.gdbus import DBus +from supervisor.utils.gdbus import DBus def test_simple_return(): diff --git a/tests/utils/test_tarfile.py b/tests/utils/test_tarfile.py index e0a5e31e7..d0b70dfe4 100644 --- a/tests/utils/test_tarfile.py +++ b/tests/utils/test_tarfile.py @@ -3,7 +3,7 @@ import attr import pytest -from hassio.utils.tar import secure_path, exclude_filter +from supervisor.utils.tar import secure_path, exclude_filter @attr.s diff --git a/tox.ini b/tox.ini index 320d34d6c..94fa7969d 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,8 @@ deps = basepython = python3 ignore_errors = True commands = - flake8 hassio - pylint --rcfile pylintrc hassio + flake8 supervisor + pylint --rcfile pylintrc supervisor [testenv:tests] basepython = python3 @@ -21,4 +21,4 @@ commands = [testenv:black] basepython = python3 commands = - black --target-version py37 --check hassio tests setup.py + black --target-version py37 --check supervisor tests setup.py