From 5497ed885a8ec3d71db0dd095f24f612850f2dd3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 17 Feb 2020 11:38:08 +0100 Subject: [PATCH 01/12] Bump version to 202 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index f840b61d5..29d135840 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -3,7 +3,7 @@ from enum import Enum from ipaddress import ip_network from pathlib import Path -HASSIO_VERSION = "201" +HASSIO_VERSION = "202" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" From a21353909d5d9c58eb82ea81d94dcab6f2bee224 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:58:00 +0100 Subject: [PATCH 02/12] Bump gitpython from 3.0.7 to 3.0.8 (#1513) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.0.7 to 3.0.8. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/master/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.0.7...3.0.8) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4955fb836..4d6fba354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ colorlog==4.1.0 cpe==1.2.1 cryptography==2.8 docker==4.2.0 -gitpython==3.0.7 +gitpython==3.0.8 packaging==20.1 pytz==2019.3 pyudev==0.22.0 From 8dbfea75b1d80f50a72c1eb8c487457e3ed567d2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 20 Feb 2020 00:29:48 +0100 Subject: [PATCH 03/12] Extend video & add tests (#1518) --- hassio/misc/hardware.py | 2 +- tests/misc/test_hardware.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/hassio/misc/hardware.py b/hassio/misc/hardware.py index 74701b330..7aa49f42b 100644 --- a/hassio/misc/hardware.py +++ b/hassio/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)") +RE_VIDEO_DEVICES = re.compile(r"^(?:vchiq|cec\d+|video\d+)") @attr.s(frozen=True) diff --git a/tests/misc/test_hardware.py b/tests/misc/test_hardware.py index d969e7152..eee1c3677 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 hassio.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( + "hassio.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"), []), + ] From 1fbb6d46ea8be17221ec09a0157945b1e5edf997 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 20 Feb 2020 09:17:53 +0100 Subject: [PATCH 04/12] Fix webui option (#1519) --- hassio/addons/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 429a2e12f..dd27eeba0 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -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 From e9f5b13aa5786d574c8e20fd171961fa42c9440b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 20 Feb 2020 21:37:59 +0100 Subject: [PATCH 05/12] Fix wrong last boot (#1521) * Protect overwrite last boot uptime * Fix naming * Fix lint --- hassio/const.py | 9 +++++++++ hassio/core.py | 36 +++++++++++++++++++++--------------- hassio/coresys.py | 5 +++-- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/hassio/const.py b/hassio/const.py index 29d135840..3d6de9fb7 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -342,3 +342,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/hassio/core.py index ae557ae60..6dfb0a758 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -1,35 +1,39 @@ -"""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 CoreSysAttributes from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError _LOGGER: logging.Logger = logging.getLogger(__name__) class HassIO(CoreSysAttributes): - """Main object of Hass.io.""" + """Main object of Supervisor.""" def __init__(self, coresys): - """Initialize Hass.io object.""" + """Initialize Supervisor object.""" self.coresys = coresys async def connect(self): """Connect Supervisor container.""" + self.coresys.state = CoreStates.INITIALIZE await self.sys_supervisor.load() async def setup(self): """Setup HassIO orchestration.""" + self.coresys.state = CoreStates.STARTUP + # Load DBus await self.sys_dbus.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,8 @@ 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.coresys.state = CoreStates.RUNNING async def stop(self): """Stop a running orchestration.""" @@ -146,7 +151,8 @@ class HassIO(CoreSysAttributes): self.sys_scheduler.suspend = True # store new last boot / prevent time adjustments - self._update_last_boot() + if self.coresys.state == CoreStates.RUNNING: + self._update_last_boot() # process async stop tasks try: @@ -163,7 +169,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 +190,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 +204,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/hassio/coresys.py index 0e63d44b7..169b225d5 100644 --- a/hassio/coresys.py +++ b/hassio/coresys.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Optional import aiohttp from .config import CoreConfig -from .const import UpdateChannels +from .const import UpdateChannels, CoreStates from .docker import DockerAPI from .misc.hardware import Hardware from .misc.scheduler import Scheduler @@ -39,7 +39,8 @@ class CoreSys: def __init__(self): """Initialize coresys.""" # Static attributes - self.machine_id: str = None + self.machine_id: Optional[str] = None + self.state: Optional[CoreStates] = None # External objects self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop() From 4ac7f7dcf08abb6ae5a018536e57d078ace046c8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 21 Feb 2020 17:55:41 +0100 Subject: [PATCH 06/12] Rename Hass.io -> Supervisor (#1522) * Rename Hass.io -> Supervisor * part 2 * fix lint * fix auth name --- .devcontainer/devcontainer.json | 51 ++++++++---------- Dockerfile | 2 +- README.md | 2 +- hassio/__init__.py | 1 - hassio/misc/__init__.py | 1 - scripts/update-frontend.sh | 4 +- setup.py | 22 ++++---- supervisor/__init__.py | 1 + {hassio => supervisor}/__main__.py | 16 +++--- {hassio => supervisor}/addons/__init__.py | 6 +-- {hassio => supervisor}/addons/addon.py | 8 +-- {hassio => supervisor}/addons/build.py | 4 +- {hassio => supervisor}/addons/data.py | 4 +- {hassio => supervisor}/addons/model.py | 10 ++-- {hassio => supervisor}/addons/utils.py | 2 +- {hassio => supervisor}/addons/validate.py | 0 {hassio => supervisor}/api/__init__.py | 10 ++-- {hassio => supervisor}/api/addons.py | 2 +- {hassio => supervisor}/api/auth.py | 4 +- {hassio => supervisor}/api/discovery.py | 2 +- {hassio => supervisor}/api/dns.py | 2 +- {hassio => supervisor}/api/hardware.py | 2 +- {hassio => supervisor}/api/hassos.py | 2 +- {hassio => supervisor}/api/homeassistant.py | 2 +- {hassio => supervisor}/api/host.py | 2 +- {hassio => supervisor}/api/info.py | 2 +- {hassio => supervisor}/api/ingress.py | 4 +- .../api/panel/201359fd5a526afe13ef.worker.js | 0 .../panel/201359fd5a526afe13ef.worker.js.gz | Bin .../panel/201359fd5a526afe13ef.worker.js.map | 0 .../api/panel/chunk.0b82745c7bdffe5c1404.js | 0 .../panel/chunk.0b82745c7bdffe5c1404.js.gz | Bin .../panel/chunk.0b82745c7bdffe5c1404.js.map | 0 .../api/panel/chunk.429840c83fad61bc51a8.js | 0 .../chunk.429840c83fad61bc51a8.js.LICENSE | 0 .../panel/chunk.429840c83fad61bc51a8.js.gz | Bin .../panel/chunk.429840c83fad61bc51a8.js.map | 0 .../api/panel/chunk.43e40fd69686ad51301d.js | 0 .../panel/chunk.43e40fd69686ad51301d.js.gz | Bin .../panel/chunk.43e40fd69686ad51301d.js.map | 0 .../api/panel/chunk.4d45ee0a3d852768f97e.js | 0 .../panel/chunk.4d45ee0a3d852768f97e.js.gz | Bin .../panel/chunk.4d45ee0a3d852768f97e.js.map | 0 .../api/panel/chunk.715824f4764bdbe425b1.js | 0 .../chunk.715824f4764bdbe425b1.js.LICENSE | 0 .../panel/chunk.715824f4764bdbe425b1.js.gz | Bin .../panel/chunk.715824f4764bdbe425b1.js.map | 0 .../api/panel/chunk.8527374a266cecf93aa9.js | 0 .../panel/chunk.8527374a266cecf93aa9.js.gz | Bin .../panel/chunk.8527374a266cecf93aa9.js.map | 0 .../api/panel/chunk.87b1d37fc9b8a6f7e2a6.js | 0 .../chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE | 0 .../panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz | Bin .../panel/chunk.87b1d37fc9b8a6f7e2a6.js.map | 0 .../api/panel/chunk.92a11ac1b80e0d7839d2.js | 0 .../panel/chunk.92a11ac1b80e0d7839d2.js.gz | Bin .../panel/chunk.92a11ac1b80e0d7839d2.js.map | 0 .../api/panel/chunk.990ee58006b248f55d23.js | 0 .../panel/chunk.990ee58006b248f55d23.js.gz | Bin .../panel/chunk.990ee58006b248f55d23.js.map | 0 .../api/panel/chunk.9d371c8143226d4eaaee.js | 0 .../chunk.9d371c8143226d4eaaee.js.LICENSE | 0 .../panel/chunk.9d371c8143226d4eaaee.js.gz | Bin .../panel/chunk.9d371c8143226d4eaaee.js.map | 0 .../api/panel/chunk.b2dce600432c76a53d8c.js | 0 .../panel/chunk.b2dce600432c76a53d8c.js.gz | Bin .../panel/chunk.b2dce600432c76a53d8c.js.map | 0 .../api/panel/chunk.b60200a57d6f63941b30.js | 0 .../panel/chunk.b60200a57d6f63941b30.js.gz | Bin .../panel/chunk.b60200a57d6f63941b30.js.map | 0 .../api/panel/chunk.d4931d72592ad48ba2be.js | 0 .../chunk.d4931d72592ad48ba2be.js.LICENSE | 0 .../panel/chunk.d4931d72592ad48ba2be.js.gz | Bin .../panel/chunk.d4931d72592ad48ba2be.js.map | 0 .../api/panel/chunk.e46c606dd9100816af4e.js | 0 .../chunk.e46c606dd9100816af4e.js.LICENSE | 0 .../panel/chunk.e46c606dd9100816af4e.js.gz | Bin .../panel/chunk.e46c606dd9100816af4e.js.map | 0 .../api/panel/chunk.f49e500cf58ea310d452.js | 0 .../chunk.f49e500cf58ea310d452.js.LICENSE | 0 .../panel/chunk.f49e500cf58ea310d452.js.gz | Bin .../panel/chunk.f49e500cf58ea310d452.js.map | 0 .../api/panel/entrypoint.582baa2f.js | 0 .../api/panel/entrypoint.582baa2f.js.gz | Bin .../api/panel/entrypoint.582baa2f.js.map | 0 .../api/panel/entrypoint.js | 0 .../api/panel/entrypoint.js.gz | Bin .../api/panel/entrypoint.js.map | 0 {hassio => supervisor}/api/panel/index.html | 0 .../api/panel/index.html.gz | Bin .../api/panel/manifest.json | 0 {hassio => supervisor}/api/proxy.py | 2 +- {hassio => supervisor}/api/security.py | 0 {hassio => supervisor}/api/services.py | 2 +- {hassio => supervisor}/api/snapshots.py | 2 +- {hassio => supervisor}/api/supervisor.py | 6 +-- {hassio => supervisor}/api/utils.py | 4 +- {hassio => supervisor}/arch.py | 0 {hassio => supervisor}/auth.py | 0 {hassio => supervisor}/bootstrap.py | 38 ++++++------- {hassio => supervisor}/config.py | 12 ++--- {hassio => supervisor}/const.py | 4 +- {hassio => supervisor}/core.py | 18 +++---- {hassio => supervisor}/coresys.py | 25 +++++---- {hassio => supervisor}/data/arch.json | 0 {hassio => supervisor}/data/asound.tmpl | 0 {hassio => supervisor}/data/audiodb.json | 0 {hassio => supervisor}/data/coredns.tmpl | 0 {hassio => supervisor}/data/hosts.tmpl | 0 {hassio => supervisor}/dbus/__init__.py | 0 {hassio => supervisor}/dbus/hostname.py | 0 {hassio => supervisor}/dbus/interface.py | 0 {hassio => supervisor}/dbus/nmi_dns.py | 0 {hassio => supervisor}/dbus/rauc.py | 0 {hassio => supervisor}/dbus/systemd.py | 0 {hassio => supervisor}/dbus/utils.py | 0 {hassio => supervisor}/discovery/__init__.py | 0 {hassio => supervisor}/discovery/const.py | 0 .../discovery/services/__init__.py | 0 .../discovery/services/adguard.py | 2 +- .../discovery/services/almond.py | 2 +- .../discovery/services/deconz.py | 2 +- .../discovery/services/home_panel.py | 2 +- .../discovery/services/mqtt.py | 2 +- .../discovery/services/unifi.py | 2 +- {hassio => supervisor}/discovery/validate.py | 2 +- {hassio => supervisor}/dns.py | 0 {hassio => supervisor}/docker/__init__.py | 4 +- {hassio => supervisor}/docker/addon.py | 4 +- {hassio => supervisor}/docker/dns.py | 4 +- {hassio => supervisor}/docker/hassos_cli.py | 2 +- .../docker/homeassistant.py | 4 +- {hassio => supervisor}/docker/interface.py | 4 +- {hassio => supervisor}/docker/network.py | 12 ++--- {hassio => supervisor}/docker/stats.py | 0 {hassio => supervisor}/docker/supervisor.py | 10 ++-- {hassio => supervisor}/exceptions.py | 0 {hassio => supervisor}/hassos.py | 2 +- {hassio => supervisor}/homeassistant.py | 2 +- {hassio => supervisor}/host/__init__.py | 0 {hassio => supervisor}/host/alsa.py | 0 {hassio => supervisor}/host/apparmor.py | 0 {hassio => supervisor}/host/control.py | 0 {hassio => supervisor}/host/info.py | 0 {hassio => supervisor}/host/network.py | 0 {hassio => supervisor}/host/services.py | 0 {hassio => supervisor}/ingress.py | 0 supervisor/misc/__init__.py | 1 + {hassio => supervisor}/misc/forwarder.py | 0 {hassio => supervisor}/misc/hardware.py | 0 {hassio => supervisor}/misc/scheduler.py | 4 +- {hassio => supervisor}/secrets.py | 0 {hassio => supervisor}/services/__init__.py | 0 {hassio => supervisor}/services/const.py | 0 {hassio => supervisor}/services/data.py | 0 {hassio => supervisor}/services/interface.py | 0 .../services/modules/__init__.py | 0 .../services/modules/mqtt.py | 6 +-- .../services/modules/mysql.py | 6 +-- {hassio => supervisor}/services/validate.py | 0 {hassio => supervisor}/snapshots/__init__.py | 0 {hassio => supervisor}/snapshots/snapshot.py | 6 +-- {hassio => supervisor}/snapshots/utils.py | 0 {hassio => supervisor}/snapshots/validate.py | 0 {hassio => supervisor}/store/__init__.py | 4 +- {hassio => supervisor}/store/addon.py | 4 +- {hassio => supervisor}/store/built-in.json | 0 {hassio => supervisor}/store/data.py | 4 +- {hassio => supervisor}/store/git.py | 8 +-- {hassio => supervisor}/store/repository.py | 4 +- {hassio => supervisor}/store/utils.py | 0 {hassio => supervisor}/store/validate.py | 0 {hassio => supervisor}/supervisor.py | 8 +-- {hassio => supervisor}/tasks.py | 8 +-- {hassio => supervisor}/updater.py | 6 +-- {hassio => supervisor}/utils/__init__.py | 2 +- {hassio => supervisor}/utils/apparmor.py | 0 {hassio => supervisor}/utils/dt.py | 2 +- {hassio => supervisor}/utils/gdbus.py | 0 {hassio => supervisor}/utils/json.py | 2 +- {hassio => supervisor}/utils/tar.py | 0 {hassio => supervisor}/utils/validate.py | 0 {hassio => supervisor}/validate.py | 0 tests/__init__.py | 2 +- tests/addons/test_config.py | 2 +- tests/addons/test_ui_schema.py | 2 +- tests/conftest.py | 14 ++--- tests/discovery/test_adguard.py | 2 +- tests/discovery/test_almond.py | 2 +- tests/discovery/test_deconz.py | 2 +- tests/discovery/test_home_panel.py | 2 +- tests/discovery/test_mqtt.py | 2 +- tests/discovery/test_unifi.py | 2 +- tests/discovery/test_validate.py | 2 +- tests/misc/test_hardware.py | 4 +- tests/test_ingress.py | 2 +- tests/test_validate.py | 20 +++---- tests/utils/test_check_port.py | 2 +- tests/utils/test_gvariant_parser.py | 2 +- tests/utils/test_tarfile.py | 2 +- tox.ini | 6 +-- 201 files changed, 244 insertions(+), 246 deletions(-) delete mode 100644 hassio/__init__.py delete mode 100644 hassio/misc/__init__.py create mode 100644 supervisor/__init__.py rename {hassio => supervisor}/__main__.py (79%) rename {hassio => supervisor}/addons/__init__.py (98%) rename {hassio => supervisor}/addons/addon.py (99%) rename {hassio => supervisor}/addons/build.py (96%) rename {hassio => supervisor}/addons/data.py (95%) rename {hassio => supervisor}/addons/model.py (98%) rename {hassio => supervisor}/addons/utils.py (98%) rename {hassio => supervisor}/addons/validate.py (100%) rename {hassio => supervisor}/api/__init__.py (98%) rename {hassio => supervisor}/api/addons.py (99%) rename {hassio => supervisor}/api/auth.py (94%) rename {hassio => supervisor}/api/discovery.py (98%) rename {hassio => supervisor}/api/dns.py (98%) rename {hassio => supervisor}/api/hardware.py (96%) rename {hassio => supervisor}/api/hassos.py (97%) rename {hassio => supervisor}/api/homeassistant.py (98%) rename {hassio => supervisor}/api/host.py (98%) rename {hassio => supervisor}/api/info.py (95%) rename {hassio => supervisor}/api/ingress.py (98%) rename {hassio => supervisor}/api/panel/201359fd5a526afe13ef.worker.js (100%) rename {hassio => supervisor}/api/panel/201359fd5a526afe13ef.worker.js.gz (100%) rename {hassio => supervisor}/api/panel/201359fd5a526afe13ef.worker.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.0b82745c7bdffe5c1404.js (100%) rename {hassio => supervisor}/api/panel/chunk.0b82745c7bdffe5c1404.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.0b82745c7bdffe5c1404.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.429840c83fad61bc51a8.js (100%) rename {hassio => supervisor}/api/panel/chunk.429840c83fad61bc51a8.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.429840c83fad61bc51a8.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.429840c83fad61bc51a8.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.43e40fd69686ad51301d.js (100%) rename {hassio => supervisor}/api/panel/chunk.43e40fd69686ad51301d.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.43e40fd69686ad51301d.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.4d45ee0a3d852768f97e.js (100%) rename {hassio => supervisor}/api/panel/chunk.4d45ee0a3d852768f97e.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.4d45ee0a3d852768f97e.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.715824f4764bdbe425b1.js (100%) rename {hassio => supervisor}/api/panel/chunk.715824f4764bdbe425b1.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.715824f4764bdbe425b1.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.715824f4764bdbe425b1.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.8527374a266cecf93aa9.js (100%) rename {hassio => supervisor}/api/panel/chunk.8527374a266cecf93aa9.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.8527374a266cecf93aa9.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js (100%) rename {hassio => supervisor}/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.87b1d37fc9b8a6f7e2a6.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.92a11ac1b80e0d7839d2.js (100%) rename {hassio => supervisor}/api/panel/chunk.92a11ac1b80e0d7839d2.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.92a11ac1b80e0d7839d2.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.990ee58006b248f55d23.js (100%) rename {hassio => supervisor}/api/panel/chunk.990ee58006b248f55d23.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.990ee58006b248f55d23.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.9d371c8143226d4eaaee.js (100%) rename {hassio => supervisor}/api/panel/chunk.9d371c8143226d4eaaee.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.9d371c8143226d4eaaee.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.9d371c8143226d4eaaee.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.b2dce600432c76a53d8c.js (100%) rename {hassio => supervisor}/api/panel/chunk.b2dce600432c76a53d8c.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.b2dce600432c76a53d8c.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.b60200a57d6f63941b30.js (100%) rename {hassio => supervisor}/api/panel/chunk.b60200a57d6f63941b30.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.b60200a57d6f63941b30.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.d4931d72592ad48ba2be.js (100%) rename {hassio => supervisor}/api/panel/chunk.d4931d72592ad48ba2be.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.d4931d72592ad48ba2be.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.d4931d72592ad48ba2be.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.e46c606dd9100816af4e.js (100%) rename {hassio => supervisor}/api/panel/chunk.e46c606dd9100816af4e.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.e46c606dd9100816af4e.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.e46c606dd9100816af4e.js.map (100%) rename {hassio => supervisor}/api/panel/chunk.f49e500cf58ea310d452.js (100%) rename {hassio => supervisor}/api/panel/chunk.f49e500cf58ea310d452.js.LICENSE (100%) rename {hassio => supervisor}/api/panel/chunk.f49e500cf58ea310d452.js.gz (100%) rename {hassio => supervisor}/api/panel/chunk.f49e500cf58ea310d452.js.map (100%) rename {hassio => supervisor}/api/panel/entrypoint.582baa2f.js (100%) rename {hassio => supervisor}/api/panel/entrypoint.582baa2f.js.gz (100%) rename {hassio => supervisor}/api/panel/entrypoint.582baa2f.js.map (100%) rename {hassio => supervisor}/api/panel/entrypoint.js (100%) rename {hassio => supervisor}/api/panel/entrypoint.js.gz (100%) rename {hassio => supervisor}/api/panel/entrypoint.js.map (100%) rename {hassio => supervisor}/api/panel/index.html (100%) rename {hassio => supervisor}/api/panel/index.html.gz (100%) rename {hassio => supervisor}/api/panel/manifest.json (100%) rename {hassio => supervisor}/api/proxy.py (99%) rename {hassio => supervisor}/api/security.py (100%) rename {hassio => supervisor}/api/services.py (97%) rename {hassio => supervisor}/api/snapshots.py (99%) rename {hassio => supervisor}/api/supervisor.py (97%) rename {hassio => supervisor}/api/utils.py (97%) rename {hassio => supervisor}/arch.py (100%) rename {hassio => supervisor}/auth.py (100%) rename {hassio => supervisor}/bootstrap.py (86%) rename {hassio => supervisor}/config.py (95%) rename {hassio => supervisor}/const.py (99%) rename {hassio => supervisor}/core.py (93%) rename {hassio => supervisor}/coresys.py (96%) rename {hassio => supervisor}/data/arch.json (100%) rename {hassio => supervisor}/data/asound.tmpl (100%) rename {hassio => supervisor}/data/audiodb.json (100%) rename {hassio => supervisor}/data/coredns.tmpl (100%) rename {hassio => supervisor}/data/hosts.tmpl (100%) rename {hassio => supervisor}/dbus/__init__.py (100%) rename {hassio => supervisor}/dbus/hostname.py (100%) rename {hassio => supervisor}/dbus/interface.py (100%) rename {hassio => supervisor}/dbus/nmi_dns.py (100%) rename {hassio => supervisor}/dbus/rauc.py (100%) rename {hassio => supervisor}/dbus/systemd.py (100%) rename {hassio => supervisor}/dbus/utils.py (100%) rename {hassio => supervisor}/discovery/__init__.py (100%) rename {hassio => supervisor}/discovery/const.py (100%) rename {hassio => supervisor}/discovery/services/__init__.py (100%) rename {hassio => supervisor}/discovery/services/adguard.py (82%) rename {hassio => supervisor}/discovery/services/almond.py (82%) rename {hassio => supervisor}/discovery/services/deconz.py (89%) rename {hassio => supervisor}/discovery/services/home_panel.py (82%) rename {hassio => supervisor}/discovery/services/mqtt.py (93%) rename {hassio => supervisor}/discovery/services/unifi.py (82%) rename {hassio => supervisor}/discovery/validate.py (93%) rename {hassio => supervisor}/dns.py (100%) rename {hassio => supervisor}/docker/__init__.py (98%) rename {hassio => supervisor}/docker/addon.py (99%) rename {hassio => supervisor}/docker/dns.py (93%) rename {hassio => supervisor}/docker/hassos_cli.py (95%) rename {hassio => supervisor}/docker/homeassistant.py (97%) rename {hassio => supervisor}/docker/interface.py (99%) rename {hassio => supervisor}/docker/network.py (91%) rename {hassio => supervisor}/docker/stats.py (100%) rename {hassio => supervisor}/docker/supervisor.py (88%) rename {hassio => supervisor}/exceptions.py (100%) rename {hassio => supervisor}/hassos.py (99%) rename {hassio => supervisor}/homeassistant.py (99%) rename {hassio => supervisor}/host/__init__.py (100%) rename {hassio => supervisor}/host/alsa.py (100%) rename {hassio => supervisor}/host/apparmor.py (100%) rename {hassio => supervisor}/host/control.py (100%) rename {hassio => supervisor}/host/info.py (100%) rename {hassio => supervisor}/host/network.py (100%) rename {hassio => supervisor}/host/services.py (100%) rename {hassio => supervisor}/ingress.py (100%) create mode 100644 supervisor/misc/__init__.py rename {hassio => supervisor}/misc/forwarder.py (100%) rename {hassio => supervisor}/misc/hardware.py (100%) rename {hassio => supervisor}/misc/scheduler.py (96%) rename {hassio => supervisor}/secrets.py (100%) rename {hassio => supervisor}/services/__init__.py (100%) rename {hassio => supervisor}/services/const.py (100%) rename {hassio => supervisor}/services/data.py (100%) rename {hassio => supervisor}/services/interface.py (100%) rename {hassio => supervisor}/services/modules/__init__.py (100%) rename {hassio => supervisor}/services/modules/mqtt.py (94%) rename {hassio => supervisor}/services/modules/mysql.py (94%) rename {hassio => supervisor}/services/validate.py (100%) rename {hassio => supervisor}/snapshots/__init__.py (100%) rename {hassio => supervisor}/snapshots/snapshot.py (99%) rename {hassio => supervisor}/snapshots/utils.py (100%) rename {hassio => supervisor}/snapshots/validate.py (100%) rename {hassio => supervisor}/store/__init__.py (97%) rename {hassio => supervisor}/store/addon.py (88%) rename {hassio => supervisor}/store/built-in.json (100%) rename {hassio => supervisor}/store/data.py (97%) rename {hassio => supervisor}/store/git.py (95%) rename {hassio => supervisor}/store/repository.py (96%) rename {hassio => supervisor}/store/utils.py (100%) rename {hassio => supervisor}/store/validate.py (100%) rename {hassio => supervisor}/supervisor.py (95%) rename {hassio => supervisor}/tasks.py (96%) rename {hassio => supervisor}/updater.py (95%) rename {hassio => supervisor}/utils/__init__.py (98%) rename {hassio => supervisor}/utils/apparmor.py (100%) rename {hassio => supervisor}/utils/dt.py (98%) rename {hassio => supervisor}/utils/gdbus.py (100%) rename {hassio => supervisor}/utils/json.py (98%) rename {hassio => supervisor}/utils/tar.py (100%) rename {hassio => supervisor}/utils/validate.py (100%) rename {hassio => supervisor}/validate.py (100%) 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/Dockerfile b/Dockerfile index 5436d632b..dbe182ec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,4 +34,4 @@ COPY entry.sh /bin/ ENTRYPOINT ["/bin/entry.sh"] WORKDIR / -CMD [ "python3", "-m", "hassio" ] +CMD [ "python3", "-m", "supervisor" ] diff --git a/README.md b/README.md index c2428cc77..ee2afd5a5 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 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/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/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..1e8ce20b2 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ 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 +24,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..46103bd4c 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 diff --git a/hassio/addons/addon.py b/supervisor/addons/addon.py similarity index 99% rename from hassio/addons/addon.py rename to supervisor/addons/addon.py index dd27eeba0..c1d840174 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 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 100% rename from hassio/addons/validate.py rename to supervisor/addons/validate.py diff --git a/hassio/api/__init__.py b/supervisor/api/__init__.py similarity index 98% rename from hassio/api/__init__.py rename to supervisor/api/__init__.py index 456ff8e7e..88b015e24 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 @@ -29,7 +29,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.""" @@ -93,7 +93,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 +165,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 +192,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), diff --git a/hassio/api/addons.py b/supervisor/api/addons.py similarity index 99% rename from hassio/api/addons.py rename to supervisor/api/addons.py index 49cdff062..d15f04040 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 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 96% rename from hassio/api/hardware.py rename to supervisor/api/hardware.py index 1d71b67f7..a26608028 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 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 100% rename from hassio/api/security.py rename to supervisor/api/security.py 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/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 86% rename from hassio/bootstrap.py rename to supervisor/bootstrap.py index ca9e930ca..aaabfbde3 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 @@ -12,7 +12,7 @@ from .api import RestAPI from .arch import CpuArch from .auth import Auth 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,11 +40,11 @@ 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.auth = Auth(coresys) @@ -89,51 +89,53 @@ 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 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 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 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() # Update log level @@ -239,7 +241,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 95% rename from hassio/config.py rename to supervisor/config.py index 7d6161c17..c70b0ccc0 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 @@ -100,7 +100,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 +119,12 @@ class CoreConfig(JsonConfig): @property def path_hassio(self): - """Return Hass.io data path.""" + """Return Supervisor data path.""" return HASSIO_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 @@ -179,12 +179,12 @@ class CoreConfig(JsonConfig): @property def path_tmp(self): - """Return Hass.io temp folder.""" + """Return Supervisor temp folder.""" return Path(HASSIO_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 diff --git a/hassio/const.py b/supervisor/const.py similarity index 99% rename from hassio/const.py rename to supervisor/const.py index 3d6de9fb7..d6766a98c 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 = "202" +SUPERVISOR_VERSION = "202" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" diff --git a/hassio/core.py b/supervisor/core.py similarity index 93% rename from hassio/core.py rename to supervisor/core.py index 6dfb0a758..e3e0c1c77 100644 --- a/hassio/core.py +++ b/supervisor/core.py @@ -12,27 +12,27 @@ from .const import ( STARTUP_SYSTEM, CoreStates, ) -from .coresys import CoreSysAttributes +from .coresys import CoreSys, CoreSysAttributes from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError _LOGGER: logging.Logger = logging.getLogger(__name__) -class HassIO(CoreSysAttributes): +class Core(CoreSysAttributes): """Main object of Supervisor.""" - def __init__(self, coresys): + def __init__(self, coresys: CoreSys): """Initialize Supervisor object.""" - self.coresys = coresys + self.coresys: CoreSys = coresys + self.state: CoreStates = CoreStates.INITIALIZE async def connect(self): """Connect Supervisor container.""" - self.coresys.state = CoreStates.INITIALIZE await self.sys_supervisor.load() async def setup(self): - """Setup HassIO orchestration.""" - self.coresys.state = CoreStates.STARTUP + """Setup supervisor orchestration.""" + self.state = CoreStates.STARTUP # Load DBus await self.sys_dbus.load() @@ -143,7 +143,7 @@ class HassIO(CoreSysAttributes): self.sys_create_task(self.sys_homeassistant.install()) _LOGGER.info("Supervisor is up and running") - self.coresys.state = CoreStates.RUNNING + self.state = CoreStates.RUNNING async def stop(self): """Stop a running orchestration.""" @@ -151,7 +151,7 @@ class HassIO(CoreSysAttributes): self.sys_scheduler.suspend = True # store new last boot / prevent time adjustments - if self.coresys.state == CoreStates.RUNNING: + if self.state == CoreStates.RUNNING: self._update_last_boot() # process async stop tasks diff --git a/hassio/coresys.py b/supervisor/coresys.py similarity index 96% rename from hassio/coresys.py rename to supervisor/coresys.py index 169b225d5..9ab44182f 100644 --- a/hassio/coresys.py +++ b/supervisor/coresys.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Optional import aiohttp from .config import CoreConfig -from .const import UpdateChannels, CoreStates +from .const import UpdateChannels from .docker import DockerAPI from .misc.hardware import Hardware from .misc.scheduler import Scheduler @@ -16,7 +16,7 @@ if TYPE_CHECKING: from .api import RestAPI from .arch import CpuArch from .auth import Auth - from .core import HassIO + from .core import Core from .dbus import DBusManager from .discovery import Discovery from .dns import CoreDNS @@ -40,7 +40,6 @@ class CoreSys: """Initialize coresys.""" # Static attributes self.machine_id: Optional[str] = None - self.state: Optional[CoreStates] = None # External objects self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop() @@ -56,7 +55,7 @@ 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._auth: Optional[Auth] = None self._dns: Optional[CoreDNS] = None @@ -78,7 +77,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 @@ -129,15 +128,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 @@ -364,7 +363,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 @@ -418,8 +417,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 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/asound.tmpl b/supervisor/data/asound.tmpl similarity index 100% rename from hassio/data/asound.tmpl rename to supervisor/data/asound.tmpl diff --git a/hassio/data/audiodb.json b/supervisor/data/audiodb.json similarity index 100% rename from hassio/data/audiodb.json rename to supervisor/data/audiodb.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/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 99% rename from hassio/docker/addon.py rename to supervisor/docker/addon.py index d6d8c4e69..a72a26df6 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 @@ -40,7 +40,7 @@ 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.""" diff --git a/hassio/docker/dns.py b/supervisor/docker/dns.py similarity index 93% rename from hassio/docker/dns.py rename to supervisor/docker/dns.py index fd489c00a..de172d506 100644 --- a/hassio/docker/dns.py +++ b/supervisor/docker/dns.py @@ -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 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 91% rename from hassio/docker/network.py rename to supervisor/docker/network.py index 610a4db61..1ed09d440 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() @@ -49,11 +49,11 @@ class DockerNetwork: return DOCKER_NETWORK_MASK[3] 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 +77,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 100% rename from hassio/exceptions.py rename to supervisor/exceptions.py diff --git a/hassio/hassos.py b/supervisor/hassos.py similarity index 99% rename from hassio/hassos.py rename to supervisor/hassos.py index 1e3b110c5..fbdf754b0 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.""" 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 100% rename from hassio/host/__init__.py rename to supervisor/host/__init__.py diff --git a/hassio/host/alsa.py b/supervisor/host/alsa.py similarity index 100% rename from hassio/host/alsa.py rename to supervisor/host/alsa.py 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/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 100% rename from hassio/misc/hardware.py rename to supervisor/misc/hardware.py 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 96% rename from hassio/tasks.py rename to supervisor/tasks.py index 2b2bc322d..80388bacb 100644 --- a/hassio/tasks.py +++ b/supervisor/tasks.py @@ -27,7 +27,7 @@ RUN_WATCHDOG_DNS_DOCKER = 20 class Tasks(CoreSysAttributes): - """Handle Tasks inside Hass.io.""" + """Handle Tasks inside Supervisor.""" def __init__(self, coresys): """Initialize Tasks.""" @@ -119,16 +119,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): diff --git a/hassio/updater.py b/supervisor/updater.py similarity index 95% rename from hassio/updater.py rename to supervisor/updater.py index 354d1c8c7..79ee549bc 100644 --- a/hassio/updater.py +++ b/supervisor/updater.py @@ -53,7 +53,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 @@ -68,12 +68,12 @@ class Updater(JsonConfig, CoreSysAttributes): @property def version_dns(self) -> Optional[str]: - """Return latest version of Hass.io DNS.""" + """Return latest version of Supervisor DNS.""" return self._data.get(ATTR_DNS) @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 diff --git a/hassio/utils/__init__.py b/supervisor/utils/__init__.py similarity index 98% rename from hassio/utils/__init__.py rename to supervisor/utils/__init__.py index 4ff19cb18..b6b29f008 100644 --- a/hassio/utils/__init__.py +++ b/supervisor/utils/__init__.py @@ -1,4 +1,4 @@ -"""Tools file for Hass.io.""" +"""Tools file for Supervisor.""" from datetime import datetime from ipaddress import IPv4Address import logging 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 100% rename from hassio/validate.py rename to supervisor/validate.py 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 eee1c3677..edc52e855 100644 --- a/tests/misc/test_hardware.py +++ b/tests/misc/test_hardware.py @@ -2,7 +2,7 @@ from unittest.mock import patch, PropertyMock from pathlib import Path -from hassio.misc.hardware import Hardware, Device +from supervisor.misc.hardware import Hardware, Device def test_read_all_devices(): @@ -23,7 +23,7 @@ def test_video_devices(): ] with patch( - "hassio.misc.hardware.Hardware.devices", new_callable=PropertyMock + "supervisor.misc.hardware.Hardware.devices", new_callable=PropertyMock ) as mock_device: mock_device.return_value = device_list 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 From 7434ca9e993235eb6a82dbee8bcbf1c791a6d6e6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2020 18:29:44 +0100 Subject: [PATCH 07/12] Bump gitpython from 3.0.8 to 3.1.0 (#1524) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.0.8 to 3.1.0. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/master/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.0.8...3.1.0) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4d6fba354..d2a4a1a57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ colorlog==4.1.0 cpe==1.2.1 cryptography==2.8 docker==4.2.0 -gitpython==3.0.8 +gitpython==3.1.0 packaging==20.1 pytz==2019.3 pyudev==0.22.0 From a3096153ab16c2731cb7544b74acaed4e1e62082 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2020 18:30:11 +0100 Subject: [PATCH 08/12] Bump gitpython from 3.0.8 to 3.1.0 (#1524) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.0.8 to 3.1.0. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/master/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.0.8...3.1.0) Signed-off-by: dependabot-preview[bot] From 0212d027fb10f6c408c0a2636e3922ac33b2750a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Feb 2020 18:37:06 +0100 Subject: [PATCH 09/12] Add Audio layer / PulseAudio (#1523) * Improve alsa handling * use default from image * create alsa folder * Map config into addon * Add Audio object * Fix dbus * add host group file * Fix persistent file * Use new template * fix lint * Fix lint * add API * Update new base image / build system * Add audio container * extend new audio settings * provide pulse client config * Adjust files * Use without auth * reset did not exists now * cleanup old alsa layer * fix tasks * fix black * fix lint * Add dbus support * add dbus adjustments * Fixups --- .devcontainer/Dockerfile | 7 + .dockerignore | 8 +- API.md | 39 +++++ Dockerfile | 14 +- MANIFEST.in | 2 +- README.md | 2 - azure-pipelines-release.yml | 7 +- build.json | 13 ++ misc/hassio.png | Bin 37464 -> 0 bytes misc/hassio.xml | 1 - rootfs/etc/cont-init.d/udev.sh | 9 ++ rootfs/etc/services.d/supervisor/finish | 5 + rootfs/etc/services.d/supervisor/run | 5 + scripts/test_env.sh | 23 ++- setup.py | 1 + supervisor/addons/__init__.py | 4 +- supervisor/addons/addon.py | 38 +++-- supervisor/addons/validate.py | 5 +- supervisor/api/__init__.py | 16 ++ supervisor/api/addons.py | 8 +- supervisor/api/audio.py | 78 ++++++++++ supervisor/api/hardware.py | 9 +- supervisor/audio.py | 199 ++++++++++++++++++++++++ supervisor/bootstrap.py | 21 ++- supervisor/config.py | 37 +++-- supervisor/const.py | 22 +-- supervisor/core.py | 4 +- supervisor/coresys.py | 19 +++ supervisor/data/asound.tmpl | 17 -- supervisor/data/audiodb.json | 18 --- supervisor/data/pulse-client.tmpl | 35 +++++ supervisor/docker/addon.py | 21 ++- supervisor/docker/audio.py | 66 ++++++++ supervisor/docker/dns.py | 3 +- supervisor/docker/network.py | 5 + supervisor/exceptions.py | 11 ++ supervisor/hassos.py | 2 +- supervisor/host/__init__.py | 7 - supervisor/host/alsa.py | 138 ---------------- supervisor/misc/hardware.py | 4 +- supervisor/tasks.py | 54 ++++--- supervisor/updater.py | 31 ++-- supervisor/utils/__init__.py | 17 +- supervisor/validate.py | 15 +- 44 files changed, 715 insertions(+), 325 deletions(-) create mode 100644 build.json delete mode 100644 misc/hassio.png delete mode 100644 misc/hassio.xml create mode 100644 rootfs/etc/cont-init.d/udev.sh create mode 100644 rootfs/etc/services.d/supervisor/finish create mode 100644 rootfs/etc/services.d/supervisor/run create mode 100644 supervisor/api/audio.py create mode 100644 supervisor/audio.py delete mode 100644 supervisor/data/asound.tmpl delete mode 100644 supervisor/data/audiodb.json create mode 100644 supervisor/data/pulse-client.tmpl create mode 100644 supervisor/docker/audio.py delete mode 100644 supervisor/host/alsa.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d48100d8d..614cd1570 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -33,6 +33,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ containerd.io \ && rm -rf /var/lib/apt/lists/* +# Install tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + jq \ + dbus \ + network-manager \ + && rm -rf /var/lib/apt/lists/* + # Install Python dependencies from requirements.txt if it exists COPY requirements.txt requirements_tests.txt ./ RUN pip3 install -r requirements.txt -r requirements_tests.txt \ 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..d2875611c 100644 --- a/API.md +++ b/API.md @@ -853,6 +853,45 @@ return: } ``` +### Audio + +- GET `/audio/info` + +```json +{ + "host": "ip-address", + "version": "1", + "latest_version": "2" +} +``` + +- POST `/audio/update` + +```json +{ + "version": "VERSION" +} +``` + +- POST `/audio/restart` + +- GET `/audio/logs` + +- GET `/audio/stats` + +```json +{ + "cpu_percent": 0.0, + "memory_usage": 283123, + "memory_limit": 329392, + "memory_percent": 1.4, + "network_tx": 0, + "network_rx": 0, + "blk_read": 0, + "blk_write": 0 +} +``` + ### Auth / SSO API You can use the user system on homeassistant. We handle this auth system on diff --git a/Dockerfile b/Dockerfile index dbe182ec8..291513258 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,15 +23,11 @@ RUN export MAKEFLAGS="-j$(nproc)" \ -r ./requirements.txt \ && rm -f requirements.txt -# Install HassIO -COPY . hassio -RUN pip3 install --no-cache-dir -e ./hassio \ - && python3 -m compileall ./hassio/hassio +# Install Home Assistant Supervisor +COPY . supervisor +RUN pip3 install --no-cache-dir -e ./supervisor \ + && python3 -m compileall ./supervisor/supervisor -# Initialize udev daemon, handle CMD -COPY entry.sh /bin/ -ENTRYPOINT ["/bin/entry.sh"] - WORKDIR / -CMD [ "python3", "-m", "supervisor" ] +COPY rootfs / 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 ee2afd5a5..c6fddb5f1 100644 --- a/README.md +++ b/README.md @@ -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-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/misc/hassio.png b/misc/hassio.png deleted file mode 100644 index 3ccbf7ae941c874face5f55e87019f7d25efc1a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37464 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 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/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/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/setup.py b/setup.py index 1e8ce20b2..85159ee8e 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +"""Home Assistant Supervisor setup.""" from setuptools import setup from supervisor.const import SUPERVISOR_VERSION diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index 46103bd4c..be2139830 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -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/supervisor/addons/addon.py b/supervisor/addons/addon.py index c1d840174..da0f4ac30 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -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/supervisor/addons/validate.py b/supervisor/addons/validate.py index ec42060f7..bda65bd84 100644 --- a/supervisor/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/supervisor/api/__init__.py b/supervisor/api/__init__.py index 88b015e24..e595c59ac 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -21,6 +21,7 @@ from .security import SecurityMiddleware from .services import APIServices from .snapshots import APISnapshots from .supervisor import APISupervisor +from .audio import APIAudio _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -314,6 +315,21 @@ class RestAPI(CoreSysAttributes): ] ) + def _register_audio(self) -> None: + """Register Audio functions.""" + api_audio = APIAudio() + api_audio.coresys = self.coresys + + self.webapp.add_routes( + [ + web.get("/audio/info", api_audio.info), + web.get("/audio/stats", api_audio.stats), + web.get("/audio/logs", api_audio.logs), + web.post("/audio/update", api_audio.update), + web.post("/audio/restart", api_audio.restart), + ] + ) + def _register_panel(self) -> None: """Register panel for Home Assistant.""" panel_dir = Path(__file__).parent.joinpath("panel") diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index d15f04040..8a11509cf 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -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..06d1b81a0 --- /dev/null +++ b/supervisor/api/audio.py @@ -0,0 +1,78 @@ +"""Init file for Supervisor Audio RESTful API.""" +import asyncio +import logging +from typing import Any, Awaitable, Dict + +from aiohttp import web +import voluptuous as vol + +from ..const import ( + ATTR_BLK_READ, + ATTR_BLK_WRITE, + ATTR_CPU_PERCENT, + ATTR_HOST, + ATTR_LATEST_VERSION, + ATTR_MEMORY_LIMIT, + ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, + ATTR_NETWORK_RX, + ATTR_NETWORK_TX, + ATTR_VERSION, + CONTENT_TYPE_BINARY, +) +from ..coresys import CoreSysAttributes +from ..exceptions import APIError +from .utils import api_process, api_process_raw, api_validate + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)}) + + +class APIAudio(CoreSysAttributes): + """Handle RESTful API for Audio functions.""" + + @api_process + async def info(self, request: web.Request) -> Dict[str, Any]: + """Return Audio information.""" + return { + ATTR_VERSION: self.sys_audio.version, + ATTR_LATEST_VERSION: self.sys_audio.latest_version, + ATTR_HOST: str(self.sys_docker.network.audio), + } + + @api_process + async def stats(self, request: web.Request) -> Dict[str, Any]: + """Return resource information.""" + stats = await self.sys_audio.stats() + + return { + ATTR_CPU_PERCENT: stats.cpu_percent, + ATTR_MEMORY_USAGE: stats.memory_usage, + ATTR_MEMORY_LIMIT: stats.memory_limit, + ATTR_MEMORY_PERCENT: stats.memory_percent, + ATTR_NETWORK_RX: stats.network_rx, + ATTR_NETWORK_TX: stats.network_tx, + ATTR_BLK_READ: stats.blk_read, + ATTR_BLK_WRITE: stats.blk_write, + } + + @api_process + async def update(self, request: web.Request) -> None: + """Update Audio plugin.""" + body = await api_validate(SCHEMA_VERSION, request) + version = body.get(ATTR_VERSION, self.sys_audio.latest_version) + + if version == self.sys_audio.version: + raise APIError("Version {} is already in use".format(version)) + await asyncio.shield(self.sys_audio.update(version)) + + @api_process_raw(CONTENT_TYPE_BINARY) + def logs(self, request: web.Request) -> Awaitable[bytes]: + """Return Audio Docker logs.""" + return self.sys_audio.logs() + + @api_process + def restart(self, request: web.Request) -> Awaitable[None]: + """Restart Audio plugin.""" + return asyncio.shield(self.sys_audio.restart()) diff --git a/supervisor/api/hardware.py b/supervisor/api/hardware.py index a26608028..7a5f4b7bd 100644 --- a/supervisor/api/hardware.py +++ b/supervisor/api/hardware.py @@ -37,13 +37,8 @@ class APIHardware(CoreSysAttributes): @api_process async def audio(self, request: web.Request) -> Dict[str, Any]: - """Show ALSA audio devices.""" - return { - ATTR_AUDIO: { - ATTR_INPUT: self.sys_host.alsa.input_devices, - ATTR_OUTPUT: self.sys_host.alsa.output_devices, - } - } + """Show pulse audio profiles.""" + return {ATTR_AUDIO: {ATTR_INPUT: [], ATTR_OUTPUT: []}} @api_process def trigger(self, request: web.Request) -> None: diff --git a/supervisor/audio.py b/supervisor/audio.py new file mode 100644 index 000000000..853f2ebe5 --- /dev/null +++ b/supervisor/audio.py @@ -0,0 +1,199 @@ +"""Home Assistant control object.""" +import asyncio +from contextlib import suppress +import logging +from pathlib import Path +from string import Template +from typing import Awaitable, Optional + +from .const import ATTR_VERSION, FILE_HASSIO_AUDIO +from .coresys import CoreSys, CoreSysAttributes +from .docker.audio import DockerAudio +from .docker.stats import DockerStats +from .exceptions import AudioError, AudioUpdateError, DockerAPIError +from .utils.json import JsonConfig +from .validate import SCHEMA_AUDIO_CONFIG + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +PULSE_CLIENT_TMPL: Path = Path(__file__).parents[0].joinpath("data/pulse-client.tmpl") + + +class Audio(JsonConfig, CoreSysAttributes): + """Home Assistant core object for handle audio.""" + + def __init__(self, coresys: CoreSys): + """Initialize hass object.""" + super().__init__(FILE_HASSIO_AUDIO, SCHEMA_AUDIO_CONFIG) + self.coresys: CoreSys = coresys + self.instance: DockerAudio = DockerAudio(coresys) + + @property + def path_extern_data(self) -> Path: + """Return path of pulse cookie file.""" + return self.sys_config.path_extern_audio.joinpath("external") + + @property + def version(self) -> Optional[str]: + """Return current version of Audio.""" + return self._data.get(ATTR_VERSION) + + @version.setter + def version(self, value: str) -> None: + """Return current version of Audio.""" + self._data[ATTR_VERSION] = value + + @property + def latest_version(self) -> Optional[str]: + """Return latest version of Audio.""" + return self.sys_updater.version_audio + + @property + def in_progress(self) -> bool: + """Return True if a task is in progress.""" + return self.instance.in_progress + + @property + def need_update(self) -> bool: + """Return True if an update is available.""" + return self.version != self.latest_version + + async def load(self) -> None: + """Load Audio setup.""" + + # Check Audio state + try: + # Evaluate Version if we lost this information + if not self.version: + self.version = await self.instance.get_latest_version(key=int) + + await self.instance.attach(tag=self.version) + except DockerAPIError: + _LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image) + + # Install CoreDNS + with suppress(AudioError): + await self.install() + else: + self.version = self.instance.version + self.save_data() + + # Run CoreDNS + with suppress(AudioError): + if await self.instance.is_running(): + await self.restart() + else: + await self.start() + + async def install(self) -> None: + """Install Audio.""" + _LOGGER.info("Setup Audio plugin") + while True: + # read audio tag and install it + if not self.latest_version: + await self.sys_updater.reload() + + if self.latest_version: + with suppress(DockerAPIError): + await self.instance.install(self.latest_version) + break + _LOGGER.warning("Error on install Audio plugin. Retry in 30sec") + await asyncio.sleep(30) + + _LOGGER.info("Audio plugin now installed") + self.version = self.instance.version + self.save_data() + + async def update(self, version: Optional[str] = None) -> None: + """Update Audio plugin.""" + version = version or self.latest_version + + if version == self.version: + _LOGGER.warning("Version %s is already installed for Audio", version) + return + + try: + await self.instance.update(version) + except DockerAPIError: + _LOGGER.error("Audio update fails") + raise AudioUpdateError() from None + else: + # Cleanup + with suppress(DockerAPIError): + await self.instance.cleanup() + + self.version = version + self.save_data() + + # Start Audio + await self.start() + + async def restart(self) -> None: + """Restart Audio plugin.""" + with suppress(DockerAPIError): + await self.instance.restart() + + async def start(self) -> None: + """Run CoreDNS.""" + # Start Instance + _LOGGER.info("Start Audio plugin") + try: + await self.instance.run() + except DockerAPIError: + _LOGGER.error("Can't start Audio plugin") + raise AudioError() from None + + def logs(self) -> Awaitable[bytes]: + """Get CoreDNS docker logs. + + Return Coroutine. + """ + return self.instance.logs() + + async def stats(self) -> DockerStats: + """Return stats of CoreDNS.""" + try: + return await self.instance.stats() + except DockerAPIError: + raise AudioError() from None + + def is_running(self) -> Awaitable[bool]: + """Return True if Docker container is running. + + Return a coroutine. + """ + return self.instance.is_running() + + def is_fails(self) -> Awaitable[bool]: + """Return True if a Docker container is fails state. + + Return a coroutine. + """ + return self.instance.is_fails() + + async def repair(self) -> None: + """Repair CoreDNS plugin.""" + if await self.instance.exists(): + return + + _LOGGER.info("Repair Audio %s", self.version) + try: + await self.instance.install(self.version) + except DockerAPIError: + _LOGGER.error("Repairing of Audio fails") + + def pulse_client(self, input_profile=None, output_profile=None) -> str: + """Generate an /etc/pulse/client.conf data.""" + + # Read Template + try: + config_data = PULSE_CLIENT_TMPL.read_text() + except OSError as err: + _LOGGER.error("Can't read pulse-client.tmpl: %s", err) + return "" + + # Process Template + config_template = Template(config_data) + return config_template.safe_substitute( + audio_address=self.sys_docker.network.audio + ) diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index aaabfbde3..4a47dc78b 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -11,6 +11,7 @@ from .addons import AddonManager from .api import RestAPI from .arch import CpuArch from .auth import Auth +from .audio import Audio from .const import SOCKET_DOCKER, UpdateChannels from .core import Core from .coresys import CoreSys @@ -47,6 +48,7 @@ async def initialize_coresys(): coresys.core = Core(coresys) coresys.dns = CoreDNS(coresys) coresys.arch = CpuArch(coresys) + coresys.audio = Audio(coresys) coresys.auth = Auth(coresys) coresys.updater = Updater(coresys) coresys.api = RestAPI(coresys) @@ -89,12 +91,12 @@ def initialize_system_data(coresys: CoreSys): ) config.path_homeassistant.mkdir() - # supervisor ssl folder + # Supervisor ssl folder if not config.path_ssl.is_dir(): _LOGGER.info("Create Supervisor SSL/TLS folder %s", config.path_ssl) config.path_ssl.mkdir() - # supervisor addon data folder + # Supervisor addon data folder if not config.path_addons_data.is_dir(): _LOGGER.info("Create Supervisor Add-on data folder %s", config.path_addons_data) config.path_addons_data.mkdir(parents=True) @@ -113,31 +115,36 @@ def initialize_system_data(coresys: CoreSys): ) config.path_addons_git.mkdir(parents=True) - # supervisor tmp folder + # Supervisor tmp folder if not config.path_tmp.is_dir(): _LOGGER.info("Create Supervisor temp folder %s", config.path_tmp) config.path_tmp.mkdir(parents=True) - # supervisor backup folder + # Supervisor backup folder if not config.path_backup.is_dir(): _LOGGER.info("Create Supervisor backup folder %s", config.path_backup) config.path_backup.mkdir() - # share folder + # Share folder if not config.path_share.is_dir(): _LOGGER.info("Create Supervisor share folder %s", config.path_share) config.path_share.mkdir() - # apparmor folder + # Apparmor folder if not config.path_apparmor.is_dir(): _LOGGER.info("Create Supervisor Apparmor folder %s", config.path_apparmor) config.path_apparmor.mkdir() - # dns folder + # DNS folder if not config.path_dns.is_dir(): _LOGGER.info("Create Supervisor DNS folder %s", config.path_dns) config.path_dns.mkdir() + # Audio folder + if not config.path_audio.is_dir(): + _LOGGER.info("Create Supervisor audio folder %s", config.path_audio) + config.path_audio.mkdir() + # Update log level coresys.config.modify_log_level() diff --git a/supervisor/config.py b/supervisor/config.py index c70b0ccc0..754e029ba 100644 --- a/supervisor/config.py +++ b/supervisor/config.py @@ -13,7 +13,7 @@ from .const import ( ATTR_TIMEZONE, ATTR_WAIT_BOOT, FILE_HASSIO_CONFIG, - HASSIO_DATA, + SUPERVISOR_DATA, ) from .utils.dt import parse_datetime from .utils.json import JsonConfig @@ -35,6 +35,7 @@ SHARE_DATA = PurePath("share") TMP_DATA = PurePath("tmp") APPARMOR_DATA = PurePath("apparmor") DNS_DATA = PurePath("dns") +AUDIO_DATA = PurePath("audio") DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() @@ -120,7 +121,7 @@ class CoreConfig(JsonConfig): @property def path_hassio(self): """Return Supervisor data path.""" - return HASSIO_DATA + return SUPERVISOR_DATA @property def path_extern_hassio(self): @@ -135,7 +136,7 @@ class CoreConfig(JsonConfig): @property def path_homeassistant(self): """Return config path inside supervisor.""" - return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG) + return Path(SUPERVISOR_DATA, HOMEASSISTANT_CONFIG) @property def path_extern_ssl(self): @@ -145,22 +146,22 @@ class CoreConfig(JsonConfig): @property def path_ssl(self): """Return SSL path inside supervisor.""" - return Path(HASSIO_DATA, HASSIO_SSL) + return Path(SUPERVISOR_DATA, HASSIO_SSL) @property def path_addons_core(self): """Return git path for core Add-ons.""" - return Path(HASSIO_DATA, ADDONS_CORE) + return Path(SUPERVISOR_DATA, ADDONS_CORE) @property def path_addons_git(self): """Return path for Git Add-on.""" - return Path(HASSIO_DATA, ADDONS_GIT) + return Path(SUPERVISOR_DATA, ADDONS_GIT) @property def path_addons_local(self): """Return path for custom Add-ons.""" - return Path(HASSIO_DATA, ADDONS_LOCAL) + return Path(SUPERVISOR_DATA, ADDONS_LOCAL) @property def path_extern_addons_local(self): @@ -170,17 +171,27 @@ class CoreConfig(JsonConfig): @property def path_addons_data(self): """Return root Add-on data folder.""" - return Path(HASSIO_DATA, ADDONS_DATA) + return Path(SUPERVISOR_DATA, ADDONS_DATA) @property def path_extern_addons_data(self): """Return root add-on data folder external for Docker.""" return PurePath(self.path_extern_hassio, ADDONS_DATA) + @property + def path_audio(self): + """Return root audio data folder.""" + return Path(SUPERVISOR_DATA, AUDIO_DATA) + + @property + def path_extern_audio(self): + """Return root audio data folder external for Docker.""" + return PurePath(self.path_extern_hassio, AUDIO_DATA) + @property def path_tmp(self): """Return Supervisor temp folder.""" - return Path(HASSIO_DATA, TMP_DATA) + return Path(SUPERVISOR_DATA, TMP_DATA) @property def path_extern_tmp(self): @@ -190,7 +201,7 @@ class CoreConfig(JsonConfig): @property def path_backup(self): """Return root backup data folder.""" - return Path(HASSIO_DATA, BACKUP_DATA) + return Path(SUPERVISOR_DATA, BACKUP_DATA) @property def path_extern_backup(self): @@ -200,12 +211,12 @@ class CoreConfig(JsonConfig): @property def path_share(self): """Return root share data folder.""" - return Path(HASSIO_DATA, SHARE_DATA) + return Path(SUPERVISOR_DATA, SHARE_DATA) @property def path_apparmor(self): """Return root Apparmor profile folder.""" - return Path(HASSIO_DATA, APPARMOR_DATA) + return Path(SUPERVISOR_DATA, APPARMOR_DATA) @property def path_extern_share(self): @@ -220,7 +231,7 @@ class CoreConfig(JsonConfig): @property def path_dns(self): """Return dns path inside supervisor.""" - return Path(HASSIO_DATA, DNS_DATA) + return Path(SUPERVISOR_DATA, DNS_DATA) @property def addons_repositories(self): diff --git a/supervisor/const.py b/supervisor/const.py index d6766a98c..e65312d7c 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -15,17 +15,18 @@ URL_HASSOS_OTA = ( "{version}/hassos_{board}-{version}.raucb" ) -HASSIO_DATA = Path("/data") +SUPERVISOR_DATA = Path("/data") -FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json") -FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json") -FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") -FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") -FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") -FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json") -FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json") -FILE_HASSIO_INGRESS = Path(HASSIO_DATA, "ingress.json") -FILE_HASSIO_DNS = Path(HASSIO_DATA, "dns.json") +FILE_HASSIO_AUTH = Path(SUPERVISOR_DATA, "auth.json") +FILE_HASSIO_ADDONS = Path(SUPERVISOR_DATA, "addons.json") +FILE_HASSIO_CONFIG = Path(SUPERVISOR_DATA, "config.json") +FILE_HASSIO_HOMEASSISTANT = Path(SUPERVISOR_DATA, "homeassistant.json") +FILE_HASSIO_UPDATER = Path(SUPERVISOR_DATA, "updater.json") +FILE_HASSIO_SERVICES = Path(SUPERVISOR_DATA, "services.json") +FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json") +FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json") +FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json") +FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json") SOCKET_DOCKER = Path("/var/run/docker.sock") @@ -229,6 +230,7 @@ ATTR_SNAPSHOT_EXCLUDE = "snapshot_exclude" ATTR_DOCUMENTATION = "documentation" ATTR_ADVANCED = "advanced" ATTR_STAGE = "stage" +ATTR_CLI = "cli" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" diff --git a/supervisor/core.py b/supervisor/core.py index e3e0c1c77..1d1328406 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -40,8 +40,8 @@ class Core(CoreSysAttributes): # Load Host await self.sys_host.load() - # Load CoreDNS - await self.sys_dns.load() + # Load Plugins container + await asyncio.wait([self.sys_dns.load(), self.sys_audio.load()]) # Load Home Assistant await self.sys_homeassistant.load() diff --git a/supervisor/coresys.py b/supervisor/coresys.py index 9ab44182f..ecfb058f2 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from .addons import AddonManager from .api import RestAPI from .arch import CpuArch + from .audio import Audio from .auth import Auth from .core import Core from .dbus import DBusManager @@ -57,6 +58,7 @@ class CoreSys: # Internal objects pointers self._core: Optional[Core] = None self._arch: Optional[CpuArch] = None + self._audio: Optional[Audio] = None self._auth: Optional[Auth] = None self._dns: Optional[CoreDNS] = None self._homeassistant: Optional[HomeAssistant] = None @@ -163,6 +165,18 @@ class CoreSys: raise RuntimeError("Auth already set!") self._auth = value + @property + def audio(self) -> Audio: + """Return Audio object.""" + return self._audio + + @audio.setter + def audio(self, value: Audio): + """Set a Audio object.""" + if self._audio: + raise RuntimeError("Audio already set!") + self._audio = value + @property def homeassistant(self) -> HomeAssistant: """Return Home Assistant object.""" @@ -431,6 +445,11 @@ class CoreSysAttributes: """Return Auth object.""" return self.coresys.auth + @property + def sys_audio(self) -> Audio: + """Return Audio object.""" + return self.coresys.audio + @property def sys_homeassistant(self) -> HomeAssistant: """Return Home Assistant object.""" diff --git a/supervisor/data/asound.tmpl b/supervisor/data/asound.tmpl deleted file mode 100644 index dc64186fd..000000000 --- a/supervisor/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/supervisor/data/audiodb.json b/supervisor/data/audiodb.json deleted file mode 100644 index f6cccd456..000000000 --- a/supervisor/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/supervisor/data/pulse-client.tmpl b/supervisor/data/pulse-client.tmpl new file mode 100644 index 000000000..1729d927f --- /dev/null +++ b/supervisor/data/pulse-client.tmpl @@ -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://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/supervisor/docker/addon.py b/supervisor/docker/addon.py index a72a26df6..f1509e720 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -35,7 +35,6 @@ if TYPE_CHECKING: _LOGGER: logging.Logger = logging.getLogger(__name__) -AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm" NO_ADDDRESS = ip_address("0.0.0.0") @@ -131,10 +130,6 @@ class DockerAddon(DockerInterface): if self.addon.devices: devices.extend(self.addon.devices) - # Use audio devices - if self.addon.with_audio and self.sys_hardware.support_audio: - devices.append(AUDIO_DEVICE) - # Auto mapping UART devices if self.addon.auto_uart: if self.addon.with_udev: @@ -298,21 +293,25 @@ class DockerAddon(DockerInterface): # Docker API support if not self.addon.protected and self.addon.access_docker_api: volumes.update( - {"/var/run/docker.sock": {"bind": "/var/run/docker.sock", "mode": "ro"}} + {"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}} ) # Host D-Bus system if self.addon.host_dbus: - volumes.update({"/var/run/dbus": {"bind": "/var/run/dbus", "mode": "rw"}}) + volumes.update({"/run/dbus": {"bind": "/run/dbus", "mode": "rw"}}) - # ALSA configuration + # Configuration Audio if self.addon.with_audio: volumes.update( { - str(self.addon.path_extern_asound): { - "bind": "/etc/asound.conf", + str(self.addon.path_extern_pulse): { + "bind": "/etc/pulse/client.conf", "mode": "ro", - } + }, + str(self.sys_audio.path_extern_data.joinpath("pulse.sock")): { + "bind": "/run/pulse.sock", + "mode": "rw", + }, } ) 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/supervisor/docker/dns.py b/supervisor/docker/dns.py index de172d506..258cc6796 100644 --- a/supervisor/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 @@ -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/supervisor/docker/network.py b/supervisor/docker/network.py index 1ed09d440..1a0fbcf4e 100644 --- a/supervisor/docker/network.py +++ b/supervisor/docker/network.py @@ -48,6 +48,11 @@ class DockerNetwork: """Return dns of the network.""" return DOCKER_NETWORK_MASK[3] + @property + def audio(self) -> IPv4Address: + """Return audio of the network.""" + return DOCKER_NETWORK_MASK[4] + def _get_network(self) -> docker.models.networks.Network: """Get supervisor network.""" try: diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index aa14db239..a89ea85fb 100644 --- a/supervisor/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 diff --git a/supervisor/hassos.py b/supervisor/hassos.py index fbdf754b0..a7f745251 100644 --- a/supervisor/hassos.py +++ b/supervisor/hassos.py @@ -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/supervisor/host/__init__.py b/supervisor/host/__init__.py index 8fe2b5404..ac1127840 100644 --- a/supervisor/host/__init__.py +++ b/supervisor/host/__init__.py @@ -2,7 +2,6 @@ from contextlib import suppress import logging -from .alsa import AlsaAudio from .apparmor import AppArmorControl from .control import SystemControl from .info import InfoCenter @@ -28,18 +27,12 @@ class HostManager(CoreSysAttributes): """Initialize Host manager.""" self.coresys: CoreSys = coresys - self._alsa: AlsaAudio = AlsaAudio(coresys) self._apparmor: AppArmorControl = AppArmorControl(coresys) self._control: SystemControl = SystemControl(coresys) self._info: InfoCenter = InfoCenter(coresys) self._services: ServiceManager = ServiceManager(coresys) self._network: NetworkManager = NetworkManager(coresys) - @property - def alsa(self) -> AlsaAudio: - """Return host ALSA handler.""" - return self._alsa - @property def apparmor(self) -> AppArmorControl: """Return host AppArmor handler.""" diff --git a/supervisor/host/alsa.py b/supervisor/host/alsa.py deleted file mode 100644 index e17082364..000000000 --- a/supervisor/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/supervisor/misc/hardware.py b/supervisor/misc/hardware.py index 7aa49f42b..7d6919c6d 100644 --- a/supervisor/misc/hardware.py +++ b/supervisor/misc/hardware.py @@ -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/supervisor/tasks.py b/supervisor/tasks.py index 80388bacb..e39d4afe9 100644 --- a/supervisor/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,6 +25,7 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15 RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_DNS_DOCKER = 20 +RUN_WATCHDOG_AUDIO_DOCKER = 20 class Tasks(CoreSysAttributes): @@ -47,13 +49,14 @@ class Tasks(CoreSysAttributes): ) ) self.jobs.add( - self.sys_scheduler.register_task( - self._update_hassos_cli, RUN_UPDATE_HASSOSCLI - ) + self.sys_scheduler.register_task(self._update_cli, RUN_UPDATE_CLI) ) self.jobs.add( self.sys_scheduler.register_task(self._update_dns, RUN_UPDATE_DNS) ) + self.jobs.add( + self.sys_scheduler.register_task(self._update_audio, RUN_UPDATE_AUDIO) + ) # Reload self.jobs.add( @@ -94,6 +97,11 @@ class Tasks(CoreSysAttributes): self._watchdog_dns_docker, RUN_WATCHDOG_DNS_DOCKER ) ) + self.jobs.add( + self.sys_scheduler.register_task( + self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER + ) + ) _LOGGER.info("All core tasks are scheduled") @@ -193,17 +201,12 @@ class Tasks(CoreSysAttributes): finally: self._cache[HASS_WATCHDOG_API] = 0 - async def _update_hassos_cli(self): - """Check and run update of HassOS CLI.""" + async def _update_cli(self): + """Check and run update of CLI.""" if not self.sys_hassos.need_cli_update: return - # don't perform an update on dev channel - if self.sys_dev: - _LOGGER.warning("Ignore HassOS CLI update on dev channel!") - return - - _LOGGER.info("Found new HassOS CLI version") + _LOGGER.info("Found new CLI version") await self.sys_hassos.update_cli() async def _update_dns(self): @@ -211,17 +214,20 @@ class Tasks(CoreSysAttributes): if not self.sys_dns.need_update: return - # don't perform an update on dev channel - if self.sys_dev: - _LOGGER.warning("Ignore CoreDNS update on dev channel!") - return - _LOGGER.info("Found new CoreDNS plugin version") await self.sys_dns.update() + async def _update_audio(self): + """Check and run update of PulseAudio plugin.""" + if not self.sys_audio.need_update: + return + + _LOGGER.info("Found new PulseAudio plugin version") + await self.sys_audio.update() + async def _watchdog_dns_docker(self): """Check running state of Docker and start if they is close.""" - # if Home Assistant is active + # if CoreDNS is active if await self.sys_dns.is_running(): return _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") @@ -234,3 +240,15 @@ class Tasks(CoreSysAttributes): await self.sys_dns.start() except CoreDNSError: _LOGGER.error("Watchdog CoreDNS reanimation fails!") + + async def _watchdog_audio_docker(self): + """Check running state of Docker and start if they is close.""" + # if PulseAudio plugin is active + if await self.sys_audio.is_running(): + return + _LOGGER.warning("Watchdog found a problem with PulseAudio plugin!") + + try: + await self.sys_audio.start() + except CoreDNSError: + _LOGGER.error("Watchdog PulseAudio reanimation fails!") diff --git a/supervisor/updater.py b/supervisor/updater.py index 79ee549bc..3765c2b05 100644 --- a/supervisor/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, @@ -62,15 +63,20 @@ class Updater(JsonConfig, CoreSysAttributes): return self._data.get(ATTR_HASSOS) @property - def version_hassos_cli(self) -> Optional[str]: - """Return latest version of HassOS cli.""" - return self._data.get(ATTR_HASSOS_CLI) + def version_cli(self) -> Optional[str]: + """Return latest version of CLI.""" + return self._data.get(ATTR_CLI) @property def version_dns(self) -> Optional[str]: - """Return latest version of Supervisor DNS.""" + """Return latest version of DNS.""" return self._data.get(ATTR_DNS) + @property + def version_audio(self) -> Optional[str]: + """Return latest version of Audio.""" + return self._data.get(ATTR_AUDIO) + @property def channel(self) -> UpdateChannels: """Return upstream channel of Supervisor instance.""" @@ -81,7 +87,7 @@ class Updater(JsonConfig, CoreSysAttributes): """Set upstream mode.""" self._data[ATTR_CHANNEL] = value - @AsyncThrottle(timedelta(seconds=60)) + @AsyncThrottle(timedelta(seconds=30)) async def fetch_data(self): """Fetch current versions from Github. @@ -110,17 +116,20 @@ class Updater(JsonConfig, CoreSysAttributes): raise HassioUpdaterError() from None try: - # update supervisor version + # Update supervisor version self._data[ATTR_HASSIO] = data["supervisor"] - self._data[ATTR_DNS] = data["dns"] - # update Home Assistant version + # Update Home Assistant core version self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine] - # update hassos version + # Update HassOS version if self.sys_hassos.available and board: self._data[ATTR_HASSOS] = data["hassos"][board] - self._data[ATTR_HASSOS_CLI] = data["hassos-cli"] + + # Update Home Assistant services + self._data[ATTR_CLI] = data["cli"] + self._data[ATTR_DNS] = data["dns"] + self._data[ATTR_AUDIO] = data["audio"] except KeyError as err: _LOGGER.warning("Can't process version data: %s", err) diff --git a/supervisor/utils/__init__.py b/supervisor/utils/__init__.py index b6b29f008..f3c0d6349 100644 --- a/supervisor/utils/__init__.py +++ b/supervisor/utils/__init__.py @@ -1,9 +1,11 @@ """Tools file for Supervisor.""" +import asyncio from datetime import datetime from ipaddress import IPv4Address import logging import re import socket +from typing import Optional _LOGGER: logging.Logger = logging.getLogger(__name__) RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))") @@ -41,18 +43,23 @@ class AsyncThrottle: """Initialize async throttle.""" self.throttle_period = delta self.time_of_last_call = datetime.min + self.synchronize: Optional[asyncio.Lock] = None def __call__(self, method): """Throttle function""" async def wrapper(*args, **kwargs): """Throttle function wrapper""" - now = datetime.now() - time_since_last_call = now - self.time_of_last_call + if not self.synchronize: + self.synchronize = asyncio.Lock() - if time_since_last_call > self.throttle_period: - self.time_of_last_call = now - return await method(*args, **kwargs) + async with self.synchronize: + now = datetime.now() + time_since_last_call = now - self.time_of_last_call + + if time_since_last_call > self.throttle_period: + self.time_of_last_call = now + return await method(*args, **kwargs) return wrapper diff --git a/supervisor/validate.py b/supervisor/validate.py index 785b6fff9..766a32d5e 100644 --- a/supervisor/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, +) From ae8ddca04054edb2553c6dca72ac3ce391c07712 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Feb 2020 18:38:52 +0100 Subject: [PATCH 10/12] Delete entry.sh --- entry.sh | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100755 entry.sh 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 From 2495cda5ecb51350a0defd848ef8c4fb2fe84f9d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 26 Feb 2020 11:48:11 +0100 Subject: [PATCH 11/12] Add Pulse audio control basics (#1525) * Add Pulse audio control basics * add functionality * Fix handling * Give access to all * Fix latest issues * revert docker * Fix pipeline --- .devcontainer/Dockerfile | 1 + API.md | 56 ++++++++++++++- Dockerfile | 17 ++--- azure-pipelines-ci.yml | 4 ++ requirements.txt | 3 +- rootfs/etc/pulse/client.conf | 35 ++++++++++ supervisor/api/__init__.py | 4 ++ supervisor/api/audio.py | 49 +++++++++++++ supervisor/api/hardware.py | 13 +++- supervisor/api/security.py | 1 + supervisor/const.py | 2 + supervisor/exceptions.py | 7 ++ supervisor/host/__init__.py | 30 +++++--- supervisor/host/sound.py | 131 +++++++++++++++++++++++++++++++++++ supervisor/updater.py | 5 +- 15 files changed, 334 insertions(+), 24 deletions(-) create mode 100644 rootfs/etc/pulse/client.conf create mode 100644 supervisor/host/sound.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 614cd1570..959c5477b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -38,6 +38,7 @@ 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 diff --git a/API.md b/API.md index d2875611c..21470355c 100644 --- a/API.md +++ b/API.md @@ -861,7 +861,25 @@ return: { "host": "ip-address", "version": "1", - "latest_version": "2" + "latest_version": "2", + "audio": { + "input": [ + { + "name": "...", + "description": "...", + "volume": 0.3, + "default": false + } + ], + "output": [ + { + "name": "...", + "description": "...", + "volume": 0.3, + "default": false + } + ] + } } ``` @@ -875,8 +893,44 @@ return: - 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 diff --git a/Dockerfile b/Dockerfile index 291513258..39089fc00 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 @@ -18,7 +19,7 @@ WORKDIR /usr/src # Install requirements COPY requirements.txt . RUN export MAKEFLAGS="-j$(nproc)" \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: \ "https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \ -r ./requirements.txt \ && rm -f requirements.txt 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/requirements.txt b/requirements.txt index d2a4a1a57..3c8914863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,10 @@ cryptography==2.8 docker==4.2.0 gitpython==3.1.0 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/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/supervisor/api/__init__.py b/supervisor/api/__init__.py index e595c59ac..bbf1ebecd 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -62,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.""" @@ -327,6 +328,9 @@ class RestAPI(CoreSysAttributes): 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), ] ) diff --git a/supervisor/api/audio.py b/supervisor/api/audio.py index 06d1b81a0..b8e9357a8 100644 --- a/supervisor/api/audio.py +++ b/supervisor/api/audio.py @@ -4,30 +4,46 @@ 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.""" @@ -39,6 +55,16 @@ class APIAudio(CoreSysAttributes): 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 @@ -76,3 +102,26 @@ class APIAudio(CoreSysAttributes): 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/supervisor/api/hardware.py b/supervisor/api/hardware.py index 7a5f4b7bd..a2bd1e2e8 100644 --- a/supervisor/api/hardware.py +++ b/supervisor/api/hardware.py @@ -38,7 +38,18 @@ class APIHardware(CoreSysAttributes): @api_process async def audio(self, request: web.Request) -> Dict[str, Any]: """Show pulse audio profiles.""" - return {ATTR_AUDIO: {ATTR_INPUT: [], ATTR_OUTPUT: []}} + return { + ATTR_AUDIO: { + 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 + }, + } + } @api_process def trigger(self, request: web.Request) -> None: diff --git a/supervisor/api/security.py b/supervisor/api/security.py index 9ce4661dd..e338600be 100644 --- a/supervisor/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/supervisor/const.py b/supervisor/const.py index e65312d7c..e00b2c1fb 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -231,6 +231,8 @@ ATTR_DOCUMENTATION = "documentation" ATTR_ADVANCED = "advanced" ATTR_STAGE = "stage" ATTR_CLI = "cli" +ATTR_DEFAULT = "default" +ATTR_VOLUME = "volume" PROVIDE_SERVICE = "provide" NEED_SERVICE = "need" diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index a89ea85fb..30b35c11e 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -210,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/supervisor/host/__init__.py b/supervisor/host/__init__.py index ac1127840..c88ec8598 100644 --- a/supervisor/host/__init__.py +++ b/supervisor/host/__init__.py @@ -2,20 +2,21 @@ from contextlib import suppress import logging +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__) @@ -32,6 +33,7 @@ class HostManager(CoreSysAttributes): self._info: InfoCenter = InfoCenter(coresys) self._services: ServiceManager = ServiceManager(coresys) self._network: NetworkManager = NetworkManager(coresys) + self._sound: SoundControl = SoundControl(coresys) @property def apparmor(self) -> AppArmorControl: @@ -58,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.""" @@ -85,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/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/supervisor/updater.py b/supervisor/updater.py index 3765c2b05..47ac77001 100644 --- a/supervisor/updater.py +++ b/supervisor/updater.py @@ -95,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) @@ -123,8 +122,8 @@ class Updater(JsonConfig, CoreSysAttributes): self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine] # Update HassOS version - if self.sys_hassos.available and board: - self._data[ATTR_HASSOS] = data["hassos"][board] + 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"] From 10230b0b4cf7fc41723f9e69ed55ddc207bed981 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 26 Feb 2020 14:28:09 +0100 Subject: [PATCH 12/12] Support profiles on template (#1527) --- Dockerfile | 2 +- requirements.txt | 1 + supervisor/audio.py | 29 ++++++++++++++++------------- supervisor/core.py | 4 ++++ supervisor/data/pulse-client.tmpl | 5 +++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 39089fc00..91329f10e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ WORKDIR /usr/src # Install requirements COPY requirements.txt . RUN export MAKEFLAGS="-j$(nproc)" \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \ "https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \ -r ./requirements.txt \ && rm -f requirements.txt diff --git a/requirements.txt b/requirements.txt index 3c8914863..55b385836 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ cpe==1.2.1 cryptography==2.8 docker==4.2.0 gitpython==3.1.0 +jinja2==2.11.1 packaging==20.1 ptvsd==4.3.2 pulsectl==20.2.2 diff --git a/supervisor/audio.py b/supervisor/audio.py index 853f2ebe5..93c6fc6ce 100644 --- a/supervisor/audio.py +++ b/supervisor/audio.py @@ -3,9 +3,10 @@ import asyncio from contextlib import suppress import logging from pathlib import Path -from string import Template 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 @@ -27,6 +28,7 @@ class Audio(JsonConfig, CoreSysAttributes): 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: @@ -60,7 +62,6 @@ class Audio(JsonConfig, CoreSysAttributes): async def load(self) -> None: """Load Audio setup.""" - # Check Audio state try: # Evaluate Version if we lost this information @@ -71,20 +72,26 @@ class Audio(JsonConfig, CoreSysAttributes): except DockerAPIError: _LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image) - # Install CoreDNS + # Install PulseAudio with suppress(AudioError): await self.install() else: self.version = self.instance.version self.save_data() - # Run CoreDNS + # 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") @@ -184,16 +191,12 @@ class Audio(JsonConfig, CoreSysAttributes): def pulse_client(self, input_profile=None, output_profile=None) -> str: """Generate an /etc/pulse/client.conf data.""" - - # Read Template - try: - config_data = PULSE_CLIENT_TMPL.read_text() - except OSError as err: - _LOGGER.error("Can't read pulse-client.tmpl: %s", err) + if self.client_template is None: return "" # Process Template - config_template = Template(config_data) - return config_template.safe_substitute( - audio_address=self.sys_docker.network.audio + return self.client_template.render( + audio_address=self.sys_docker.network.audio, + default_source=input_profile, + default_sink=output_profile, ) diff --git a/supervisor/core.py b/supervisor/core.py index 1d1328406..dd2e78e17 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -145,6 +145,10 @@ class Core(CoreSysAttributes): _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.""" # don't process scheduler anymore diff --git a/supervisor/data/pulse-client.tmpl b/supervisor/data/pulse-client.tmpl index 1729d927f..9488d23cd 100644 --- a/supervisor/data/pulse-client.tmpl +++ b/supervisor/data/pulse-client.tmpl @@ -17,8 +17,9 @@ ## more information. Default values are commented out. Use either ; or # for ## commenting. -; default-sink = -; default-source = +{% 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 =