Group all homeassistant/core into a module (#1950)

* Group all homeassistant/core into a module

* fix references

* fix lint

* streamline object property protection

* Fix api
This commit is contained in:
Pascal Vizeli 2020-08-20 11:22:04 +02:00 committed by GitHub
parent b3308ecbe0
commit 2411b4287d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 467 additions and 414 deletions

View File

@ -359,7 +359,7 @@ class Addon(AddonModel):
options = self.options
# Update secrets for validation
await self.sys_secrets.reload()
await self.sys_homeassistant.secrets.reload()
try:
options = schema(options)

View File

@ -282,7 +282,7 @@ class APIAddons(CoreSysAttributes):
addon = self._extract_addon_installed(request)
# Update secrets for validation
await self.sys_secrets.reload()
await self.sys_homeassistant.secrets.reload()
# Extend schema with add-on specific validation
addon_schema = SCHEMA_OPTIONS.extend(

View File

@ -117,7 +117,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process
async def stats(self, request: web.Request) -> Dict[Any, str]:
"""Return resource information."""
stats = await self.sys_homeassistant.stats()
stats = await self.sys_homeassistant.core.stats()
if not stats:
raise APIError("No stats available")
@ -138,36 +138,36 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
await asyncio.shield(self.sys_homeassistant.update(version))
await asyncio.shield(self.sys_homeassistant.core.update(version))
@api_process
def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.stop())
return asyncio.shield(self.sys_homeassistant.core.stop())
@api_process
def start(self, request: web.Request) -> Awaitable[None]:
"""Start Home Assistant."""
return asyncio.shield(self.sys_homeassistant.start())
return asyncio.shield(self.sys_homeassistant.core.start())
@api_process
def restart(self, request: web.Request) -> Awaitable[None]:
"""Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.restart())
return asyncio.shield(self.sys_homeassistant.core.restart())
@api_process
def rebuild(self, request: web.Request) -> Awaitable[None]:
"""Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.rebuild())
return asyncio.shield(self.sys_homeassistant.core.rebuild())
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return Home Assistant Docker logs."""
return self.sys_homeassistant.logs()
return self.sys_homeassistant.core.logs()
@api_process
async def check(self, request: web.Request) -> None:
"""Check configuration of Home Assistant."""
result = await self.sys_homeassistant.check_config()
result = await self.sys_homeassistant.core.check_config()
if not result.valid:
raise APIError(result.log)

View File

@ -45,7 +45,7 @@ class APIProxy(CoreSysAttributes):
async def _api_client(self, request: web.Request, path: str, timeout: int = 300):
"""Return a client request with proxy origin for Home Assistant."""
try:
async with self.sys_homeassistant.make_request(
async with self.sys_homeassistant.api.make_request(
request.method.lower(),
f"api/{path}",
headers={
@ -75,7 +75,7 @@ class APIProxy(CoreSysAttributes):
async def stream(self, request: web.Request):
"""Proxy HomeAssistant EventStream Requests."""
self._check_access(request)
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
raise HTTPBadGateway()
_LOGGER.info("Home Assistant EventStream start")
@ -96,7 +96,7 @@ class APIProxy(CoreSysAttributes):
async def api(self, request: web.Request):
"""Proxy Home Assistant API Requests."""
self._check_access(request)
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
raise HTTPBadGateway()
# Normal request
@ -130,7 +130,10 @@ class APIProxy(CoreSysAttributes):
# Auth session
await self.sys_homeassistant.ensure_access_token()
await client.send_json(
{"type": "auth", "access_token": self.sys_homeassistant.access_token}
{
"type": "auth",
"access_token": self.sys_homeassistant.api.access_token,
}
)
data = await client.receive_json()
@ -143,7 +146,7 @@ class APIProxy(CoreSysAttributes):
data.get("type") == "invalid_auth"
and self.sys_homeassistant.refresh_token
):
self.sys_homeassistant.access_token = None
self.sys_homeassistant.api.access_token = None
return await self._websocket_client()
raise HomeAssistantAuthError()
@ -157,7 +160,7 @@ class APIProxy(CoreSysAttributes):
async def websocket(self, request: web.Request):
"""Initialize a WebSocket API connection."""
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
raise HTTPBadGateway()
_LOGGER.info("Home Assistant WebSocket API request initialize")

View File

@ -176,7 +176,9 @@ class APISupervisor(CoreSysAttributes):
def reload(self, request: web.Request) -> Awaitable[None]:
"""Reload add-ons, configuration, etc."""
return asyncio.shield(
asyncio.wait([self.sys_updater.reload(), self.sys_secrets.reload()])
asyncio.wait(
[self.sys_updater.reload(), self.sys_homeassistant.secrets.reload()]
)
)
@api_process

View File

@ -62,12 +62,12 @@ class Auth(JsonConfig, CoreSysAttributes):
_LOGGER.info("Auth request from %s for %s", addon.slug, username)
# Check API state
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
_LOGGER.info("Home Assistant not running, check cache")
return self._check_cache(username, password)
try:
async with self.sys_homeassistant.make_request(
async with self.sys_homeassistant.api.make_request(
"post",
"api/hassio_auth",
json={
@ -93,7 +93,7 @@ class Auth(JsonConfig, CoreSysAttributes):
async def change_password(self, username: str, password: str) -> None:
"""Change user password login."""
try:
async with self.sys_homeassistant.make_request(
async with self.sys_homeassistant.api.make_request(
"post",
"api/hassio_auth/password_reset",
json={ATTR_USERNAME: username, ATTR_PASSWORD: password},

View File

@ -37,7 +37,6 @@ from .ingress import Ingress
from .misc.filter import filter_data
from .misc.hwmon import HwMonitor
from .misc.scheduler import Scheduler
from .misc.secrets import SecretsManager
from .misc.tasks import Tasks
from .plugins import PluginManager
from .services import ServiceManager
@ -74,7 +73,6 @@ async def initialize_coresys() -> CoreSys:
coresys.discovery = Discovery(coresys)
coresys.dbus = DBusManager(coresys)
coresys.hassos = HassOS(coresys)
coresys.secrets = SecretsManager(coresys)
coresys.scheduler = Scheduler(coresys)
# diagnostics

View File

@ -122,9 +122,6 @@ class Core(CoreSysAttributes):
# Load ingress
await self.sys_ingress.load()
# Load secrets
await self.sys_secrets.load()
# Check supported OS
if not self.sys_hassos.available:
if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS:
@ -204,10 +201,10 @@ class Core(CoreSysAttributes):
# run HomeAssistant
if (
self.sys_homeassistant.boot
and not await self.sys_homeassistant.is_running()
and not await self.sys_homeassistant.core.is_running()
):
with suppress(HomeAssistantError):
await self.sys_homeassistant.start()
await self.sys_homeassistant.core.start()
else:
_LOGGER.info("Skip start of Home Assistant")
@ -223,7 +220,7 @@ class Core(CoreSysAttributes):
# If landingpage / run upgrade in background
if self.sys_homeassistant.version == "landingpage":
self.sys_create_task(self.sys_homeassistant.install())
self.sys_create_task(self.sys_homeassistant.core.install())
# Start observe the host Hardware
await self.sys_hwmonitor.load()
@ -278,7 +275,7 @@ class Core(CoreSysAttributes):
# Close Home Assistant
with suppress(HassioError):
await self.sys_homeassistant.stop()
await self.sys_homeassistant.core.stop()
# Shutdown System Add-ons
await self.sys_addons.shutdown(AddonStartup.SERVICES)
@ -304,7 +301,7 @@ class Core(CoreSysAttributes):
# Restore core functionality
await self.sys_addons.repair()
await self.sys_homeassistant.repair()
await self.sys_homeassistant.core.repair()
# Tag version for latest
await self.sys_supervisor.repair()

View File

@ -23,7 +23,6 @@ if TYPE_CHECKING:
from .hassos import HassOS
from .misc.scheduler import Scheduler
from .misc.hwmon import HwMonitor
from .misc.secrets import SecretsManager
from .misc.tasks import Tasks
from .homeassistant import HomeAssistant
from .host import HostManager
@ -76,7 +75,6 @@ class CoreSys:
self._dbus: Optional[DBusManager] = None
self._hassos: Optional[HassOS] = None
self._services: Optional[ServiceManager] = None
self._secrets: Optional[SecretsManager] = None
self._scheduler: Optional[Scheduler] = None
self._store: Optional[StoreManager] = None
self._discovery: Optional[Discovery] = None
@ -246,20 +244,6 @@ class CoreSys:
raise RuntimeError("Updater already set!")
self._updater = value
@property
def secrets(self) -> SecretsManager:
"""Return SecretsManager object."""
if self._secrets is None:
raise RuntimeError("SecretsManager not set!")
return self._secrets
@secrets.setter
def secrets(self, value: SecretsManager) -> None:
"""Set a Updater object."""
if self._secrets:
raise RuntimeError("SecretsManager already set!")
self._secrets = value
@property
def addons(self) -> AddonManager:
"""Return AddonManager object."""
@ -529,11 +513,6 @@ class CoreSysAttributes:
"""Return Updater object."""
return self.coresys.updater
@property
def sys_secrets(self) -> SecretsManager:
"""Return SecretsManager object."""
return self.coresys.secrets
@property
def sys_addons(self) -> AddonManager:
"""Return AddonManager object."""

View File

@ -117,7 +117,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
async def _push_discovery(self, message: Message, command: str) -> None:
"""Send a discovery request."""
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
_LOGGER.info("Discovery %s message ignore", message.uuid)
return
@ -125,7 +125,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
data.pop(ATTR_CONFIG)
with suppress(HomeAssistantAPIError):
async with self.sys_homeassistant.make_request(
async with self.sys_homeassistant.api.make_request(
command,
f"api/hassio_push/discovery/{message.uuid}",
json=data,

View File

@ -0,0 +1,241 @@
"""Home Assistant control object."""
import asyncio
from ipaddress import IPv4Address
import logging
from pathlib import Path
import shutil
from typing import Optional
from uuid import UUID
from ..const import (
ATTR_ACCESS_TOKEN,
ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT,
ATTR_BOOT,
ATTR_IMAGE,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SSL,
ATTR_UUID,
ATTR_VERSION,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG,
FILE_HASSIO_HOMEASSISTANT,
)
from ..coresys import CoreSys, CoreSysAttributes
from ..utils.json import JsonConfig
from ..validate import SCHEMA_HASS_CONFIG
from .api import HomeAssistantAPI
from .core import HomeAssistantCore
from .secrets import HomeAssistantSecrets
_LOGGER: logging.Logger = logging.getLogger(__name__)
class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Home Assistant core object for handle it."""
def __init__(self, coresys: CoreSys):
"""Initialize Home Assistant object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.coresys: CoreSys = coresys
self._api: HomeAssistantAPI = HomeAssistantAPI(coresys)
self._core: HomeAssistantCore = HomeAssistantCore(coresys)
self._secrets: HomeAssistantSecrets = HomeAssistantSecrets(coresys)
@property
def api(self) -> HomeAssistantAPI:
"""Return API handler for core."""
return self._api
@property
def core(self) -> HomeAssistantCore:
"""Return Core handler for docker."""
return self._core
@property
def secrets(self) -> HomeAssistantSecrets:
"""Return Secrets Manager for core."""
return self._secrets
@property
def machine(self) -> str:
"""Return the system machines."""
return self.core.instance.machine
@property
def arch(self) -> str:
"""Return arch of running Home Assistant."""
return self.core.instance.arch
@property
def error_state(self) -> bool:
"""Return True if system is in error."""
return self.core.error_state
@property
def ip_address(self) -> IPv4Address:
"""Return IP of Home Assistant instance."""
return self.core.instance.ip_address
@property
def api_port(self) -> int:
"""Return network port to Home Assistant instance."""
return self._data[ATTR_PORT]
@api_port.setter
def api_port(self, value: int) -> None:
"""Set network port for Home Assistant instance."""
self._data[ATTR_PORT] = value
@property
def api_ssl(self) -> bool:
"""Return if we need ssl to Home Assistant instance."""
return self._data[ATTR_SSL]
@api_ssl.setter
def api_ssl(self, value: bool):
"""Set SSL for Home Assistant instance."""
self._data[ATTR_SSL] = value
@property
def api_url(self) -> str:
"""Return API url to Home Assistant."""
return "{}://{}:{}".format(
"https" if self.api_ssl else "http", self.ip_address, self.api_port
)
@property
def watchdog(self) -> bool:
"""Return True if the watchdog should protect Home Assistant."""
return self._data[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value: bool):
"""Return True if the watchdog should protect Home Assistant."""
self._data[ATTR_WATCHDOG] = value
@property
def wait_boot(self) -> int:
"""Return time to wait for Home Assistant startup."""
return self._data[ATTR_WAIT_BOOT]
@wait_boot.setter
def wait_boot(self, value: int):
"""Set time to wait for Home Assistant startup."""
self._data[ATTR_WAIT_BOOT] = value
@property
def latest_version(self) -> str:
"""Return last available version of Home Assistant."""
return self.sys_updater.version_homeassistant
@property
def image(self) -> str:
"""Return image name of the Home Assistant container."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_machine}-homeassistant"
@image.setter
def image(self, value: str) -> None:
"""Set image name of Home Assistant container."""
self._data[ATTR_IMAGE] = value
@property
def version(self) -> Optional[str]:
"""Return version of local version."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set installed version."""
self._data[ATTR_VERSION] = value
@property
def boot(self) -> bool:
"""Return True if Home Assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value: bool):
"""Set Home Assistant boot options."""
self._data[ATTR_BOOT] = value
@property
def uuid(self) -> UUID:
"""Return a UUID of this Home Assistant instance."""
return self._data[ATTR_UUID]
@property
def supervisor_token(self) -> Optional[str]:
"""Return an access token for the Supervisor API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@supervisor_token.setter
def supervisor_token(self, value: str) -> None:
"""Set the access token for the Supervisor API."""
self._data[ATTR_ACCESS_TOKEN] = value
@property
def refresh_token(self) -> Optional[str]:
"""Return the refresh token to authenticate with Home Assistant."""
return self._data.get(ATTR_REFRESH_TOKEN)
@refresh_token.setter
def refresh_token(self, value: str):
"""Set Home Assistant refresh_token."""
self._data[ATTR_REFRESH_TOKEN] = value
@property
def path_pulse(self):
"""Return path to asound config."""
return Path(self.sys_config.path_tmp, "homeassistant_pulse")
@property
def path_extern_pulse(self):
"""Return path to asound config for Docker."""
return Path(self.sys_config.path_extern_tmp, "homeassistant_pulse")
@property
def audio_output(self) -> Optional[str]:
"""Return a pulse profile for output or None."""
return self._data[ATTR_AUDIO_OUTPUT]
@audio_output.setter
def audio_output(self, value: Optional[str]):
"""Set audio output profile settings."""
self._data[ATTR_AUDIO_OUTPUT] = value
@property
def audio_input(self) -> Optional[str]:
"""Return pulse profile for input or None."""
return self._data[ATTR_AUDIO_INPUT]
@audio_input.setter
def audio_input(self, value: Optional[str]):
"""Set audio input settings."""
self._data[ATTR_AUDIO_INPUT] = value
async def load(self) -> None:
"""Prepare Home Assistant object."""
await asyncio.wait([self.secrets.load(), self.core.load()])
def write_pulse(self):
"""Write asound config to file and return True on success."""
pulse_config = self.sys_plugins.audio.pulse_client(
input_profile=self.audio_input, output_profile=self.audio_output
)
# Cleanup wrong maps
if self.path_pulse.is_dir():
shutil.rmtree(self.path_pulse, ignore_errors=True)
# Write pulse config
try:
with self.path_pulse.open("w") as config_file:
config_file.write(pulse_config)
except OSError as err:
_LOGGER.error("Home Assistant can't write pulse/client.config: %s", err)
else:
_LOGGER.info("Update pulse/client.config: %s", self.path_pulse)

View File

@ -0,0 +1,122 @@
"""Home Assistant control object."""
import asyncio
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
import logging
from typing import Any, AsyncContextManager, Dict, Optional
import aiohttp
from aiohttp import hdrs
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HomeAssistantAPIError, HomeAssistantAuthError
from ..utils import check_port
_LOGGER: logging.Logger = logging.getLogger(__name__)
class HomeAssistantAPI(CoreSysAttributes):
"""Home Assistant core object for handle it."""
def __init__(self, coresys: CoreSys):
"""Initialize Home Assistant object."""
self.coresys: CoreSys = coresys
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token: Optional[str] = None
self._access_token_expires: Optional[datetime] = None
async def _ensure_access_token(self) -> None:
"""Ensure there is an access token."""
if (
self.access_token is not None
and self._access_token_expires > datetime.utcnow()
):
return
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
async with self.sys_websession_ssl.post(
f"{self.sys_homeassistant.api_url}/auth/token",
timeout=30,
data={
"grant_type": "refresh_token",
"refresh_token": self.sys_homeassistant.refresh_token,
},
) as resp:
if resp.status != 200:
_LOGGER.error("Can't update Home Assistant access token!")
raise HomeAssistantAuthError()
_LOGGER.info("Updated Home Assistant API token")
tokens = await resp.json()
self.access_token = tokens["access_token"]
self._access_token_expires = datetime.utcnow() + timedelta(
seconds=tokens["expires_in"]
)
@asynccontextmanager
async def make_request(
self,
method: str,
path: str,
json: Optional[Dict[str, Any]] = None,
content_type: Optional[str] = None,
data: Any = None,
timeout: int = 30,
params: Optional[Dict[str, str]] = None,
headers: Optional[Dict[str, str]] = None,
) -> AsyncContextManager[aiohttp.ClientResponse]:
"""Async context manager to make a request with right auth."""
url = f"{self.sys_homeassistant.api_url}/{path}"
headers = headers or {}
# Passthrough content type
if content_type is not None:
headers[hdrs.CONTENT_TYPE] = content_type
for _ in (1, 2):
await self._ensure_access_token()
headers[hdrs.AUTHORIZATION] = f"Bearer {self.access_token}"
try:
async with getattr(self.sys_websession_ssl, method)(
url,
data=data,
timeout=timeout,
json=json,
headers=headers,
params=params,
) as resp:
# Access token expired
if resp.status == 401:
self.access_token = None
continue
yield resp
return
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error on call %s: %s", url, err)
break
raise HomeAssistantAPIError()
async def check_api_state(self) -> bool:
"""Return True if Home Assistant up and running."""
# Check if port is up
if not await self.sys_run_in_executor(
check_port,
self.sys_homeassistant.ip_address,
self.sys_homeassistant.api_port,
):
return False
# Check if API is up
with suppress(HomeAssistantAPIError):
async with self.make_request("get", "api/config") as resp:
if resp.status in (200, 201):
data = await resp.json()
if data.get("state", "RUNNING") == "RUNNING":
return True
else:
_LOGGER.debug("Home Assistant API return: %d", resp.status)
return False

View File

@ -1,50 +1,22 @@
"""Home Assistant control object."""
import asyncio
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
from ipaddress import IPv4Address
from contextlib import suppress
import logging
from pathlib import Path
import re
import secrets
import shutil
import time
from typing import Any, AsyncContextManager, Awaitable, Dict, Optional
from uuid import UUID
from typing import Awaitable, Optional
import aiohttp
from aiohttp import hdrs
import attr
from packaging import version as pkg_version
from .const import (
ATTR_ACCESS_TOKEN,
ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT,
ATTR_BOOT,
ATTR_IMAGE,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SSL,
ATTR_UUID,
ATTR_VERSION,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG,
FILE_HASSIO_HOMEASSISTANT,
)
from .coresys import CoreSys, CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant
from .docker.stats import DockerStats
from .exceptions import (
DockerAPIError,
HomeAssistantAPIError,
HomeAssistantAuthError,
HomeAssistantError,
HomeAssistantUpdateError,
)
from .utils import check_port, convert_to_ascii, process_lock
from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG
from ..coresys import CoreSys, CoreSysAttributes
from ..docker.homeassistant import DockerHomeAssistant
from ..docker.stats import DockerStats
from ..exceptions import DockerAPIError, HomeAssistantError, HomeAssistantUpdateError
from ..utils import convert_to_ascii, process_lock
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -61,192 +33,40 @@ class ConfigResult:
log = attr.ib()
class HomeAssistant(JsonConfig, CoreSysAttributes):
class HomeAssistantCore(CoreSysAttributes):
"""Home Assistant core object for handle it."""
def __init__(self, coresys: CoreSys):
"""Initialize Home Assistant object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.coresys: CoreSys = coresys
self.instance: DockerHomeAssistant = DockerHomeAssistant(coresys)
self.lock: asyncio.Lock = asyncio.Lock()
self._error_state: bool = False
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token: Optional[str] = None
self._access_token_expires: Optional[datetime] = None
async def load(self) -> None:
"""Prepare Home Assistant object."""
try:
# Evaluate Version if we lost this information
if not self.version:
self.version = await self.instance.get_latest_version(
key=pkg_version.parse
)
await self.instance.attach(tag=self.version)
except DockerAPIError:
_LOGGER.info("No Home Assistant Docker image %s found.", self.image)
await self.install_landingpage()
else:
self.version = self.instance.version
self.image = self.instance.image
self.save_data()
@property
def machine(self) -> str:
"""Return the system machines."""
return self.instance.machine
@property
def arch(self) -> str:
"""Return arch of running Home Assistant."""
return self.instance.arch
@property
def error_state(self) -> bool:
"""Return True if system is in error."""
return self._error_state
@property
def ip_address(self) -> IPv4Address:
"""Return IP of Home Assistant instance."""
return self.instance.ip_address
async def load(self) -> None:
"""Prepare Home Assistant object."""
try:
# Evaluate Version if we lost this information
if not self.sys_homeassistant.version:
self.sys_homeassistant.version = await self.instance.get_latest_version(
key=pkg_version.parse
)
@property
def api_port(self) -> int:
"""Return network port to Home Assistant instance."""
return self._data[ATTR_PORT]
@api_port.setter
def api_port(self, value: int) -> None:
"""Set network port for Home Assistant instance."""
self._data[ATTR_PORT] = value
@property
def api_ssl(self) -> bool:
"""Return if we need ssl to Home Assistant instance."""
return self._data[ATTR_SSL]
@api_ssl.setter
def api_ssl(self, value: bool):
"""Set SSL for Home Assistant instance."""
self._data[ATTR_SSL] = value
@property
def api_url(self) -> str:
"""Return API url to Home Assistant."""
return "{}://{}:{}".format(
"https" if self.api_ssl else "http", self.ip_address, self.api_port
)
@property
def watchdog(self) -> bool:
"""Return True if the watchdog should protect Home Assistant."""
return self._data[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value: bool):
"""Return True if the watchdog should protect Home Assistant."""
self._data[ATTR_WATCHDOG] = value
@property
def wait_boot(self) -> int:
"""Return time to wait for Home Assistant startup."""
return self._data[ATTR_WAIT_BOOT]
@wait_boot.setter
def wait_boot(self, value: int):
"""Set time to wait for Home Assistant startup."""
self._data[ATTR_WAIT_BOOT] = value
@property
def latest_version(self) -> str:
"""Return last available version of Home Assistant."""
return self.sys_updater.version_homeassistant
@property
def image(self) -> str:
"""Return image name of the Home Assistant container."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_machine}-homeassistant"
@image.setter
def image(self, value: str) -> None:
"""Set image name of Home Assistant container."""
self._data[ATTR_IMAGE] = value
@property
def version(self) -> Optional[str]:
"""Return version of local version."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set installed version."""
self._data[ATTR_VERSION] = value
@property
def boot(self) -> bool:
"""Return True if Home Assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value: bool):
"""Set Home Assistant boot options."""
self._data[ATTR_BOOT] = value
@property
def uuid(self) -> UUID:
"""Return a UUID of this Home Assistant instance."""
return self._data[ATTR_UUID]
@property
def supervisor_token(self) -> Optional[str]:
"""Return an access token for the Supervisor API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@property
def refresh_token(self) -> Optional[str]:
"""Return the refresh token to authenticate with Home Assistant."""
return self._data.get(ATTR_REFRESH_TOKEN)
@refresh_token.setter
def refresh_token(self, value: str):
"""Set Home Assistant refresh_token."""
self._data[ATTR_REFRESH_TOKEN] = value
@property
def path_pulse(self):
"""Return path to asound config."""
return Path(self.sys_config.path_tmp, "homeassistant_pulse")
@property
def path_extern_pulse(self):
"""Return path to asound config for Docker."""
return Path(self.sys_config.path_extern_tmp, "homeassistant_pulse")
@property
def audio_output(self) -> Optional[str]:
"""Return a pulse profile for output or None."""
return self._data[ATTR_AUDIO_OUTPUT]
@audio_output.setter
def audio_output(self, value: Optional[str]):
"""Set audio output profile settings."""
self._data[ATTR_AUDIO_OUTPUT] = value
@property
def audio_input(self) -> Optional[str]:
"""Return pulse profile for input or None."""
return self._data[ATTR_AUDIO_INPUT]
@audio_input.setter
def audio_input(self, value: Optional[str]):
"""Set audio input settings."""
self._data[ATTR_AUDIO_INPUT] = value
await self.instance.attach(tag=self.sys_homeassistant.version)
except DockerAPIError:
_LOGGER.info(
"No Home Assistant Docker image %s found.", self.sys_homeassistant.image
)
await self.install_landingpage()
else:
self.sys_homeassistant.version = self.instance.version
self.sys_homeassistant.image = self.instance.image
self.sys_homeassistant.save_data()
@process_lock
async def install_landingpage(self) -> None:
@ -271,9 +91,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
except Exception as err: # pylint: disable=broad-except
self.sys_capture_exception(err)
else:
self.version = self.instance.version
self.image = self.sys_updater.image_homeassistant
self.save_data()
self.sys_homeassistant.version = self.instance.version
self.sys_homeassistant.image = self.sys_updater.image_homeassistant
self.sys_homeassistant.save_data()
break
# Start landingpage
@ -287,10 +107,10 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.info("Setup Home Assistant")
while True:
# read homeassistant tag and install it
if not self.latest_version:
if not self.sys_homeassistant.latest_version:
await self.sys_updater.reload()
tag = self.latest_version
tag = self.sys_homeassistant.latest_version
if tag:
try:
await self.instance.update(
@ -306,9 +126,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await asyncio.sleep(30)
_LOGGER.info("Home Assistant docker now installed")
self.version = self.instance.version
self.image = self.sys_updater.image_homeassistant
self.save_data()
self.sys_homeassistant.version = self.instance.version
self.sys_homeassistant.image = self.sys_updater.image_homeassistant
self.sys_homeassistant.save_data()
# finishing
try:
@ -324,9 +144,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
@process_lock
async def update(self, version: Optional[str] = None) -> None:
"""Update HomeAssistant version."""
version = version or self.latest_version
old_image = self.image
rollback = self.version if not self.error_state else None
version = version or self.sys_homeassistant.latest_version
old_image = self.sys_homeassistant.image
rollback = self.sys_homeassistant.version if not self.error_state else None
running = await self.instance.is_running()
exists = await self.instance.exists()
@ -346,15 +166,15 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Update Home Assistant image fails")
raise HomeAssistantUpdateError()
else:
self.version = self.instance.version
self.image = self.sys_updater.image_homeassistant
self.sys_homeassistant.version = self.instance.version
self.sys_homeassistant.image = self.sys_updater.image_homeassistant
if running:
await self._start()
_LOGGER.info("Successful run Home Assistant %s", to_version)
# Successfull - last step
self.save_data()
self.sys_homeassistant.save_data()
with suppress(DockerAPIError):
await self.instance.cleanup(old_image=old_image)
@ -384,18 +204,18 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def _start(self) -> None:
"""Start Home Assistant Docker & wait."""
# Create new API token
self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
self.save_data()
self.sys_homeassistant.supervisor_token = secrets.token_hex(56)
self.sys_homeassistant.save_data()
# Write audio settings
self.write_pulse()
self.sys_homeassistant.write_pulse()
try:
await self.instance.run()
except DockerAPIError:
raise HomeAssistantError()
await self._block_till_run(self.version)
await self._block_till_run(self.sys_homeassistant.version)
@process_lock
async def start(self) -> None:
@ -411,7 +231,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
except DockerAPIError:
raise HomeAssistantError()
await self._block_till_run(self.version)
await self._block_till_run(self.sys_homeassistant.version)
# No Instance/Container found, extended start
else:
await self._start()
@ -435,7 +255,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
except DockerAPIError:
raise HomeAssistantError()
await self._block_till_run(self.version)
await self._block_till_run(self.sys_homeassistant.version)
@process_lock
async def rebuild(self) -> None:
@ -503,101 +323,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.info("Home Assistant config is valid")
return ConfigResult(True, log)
async def ensure_access_token(self) -> None:
"""Ensure there is an access token."""
if (
self.access_token is not None
and self._access_token_expires > datetime.utcnow()
):
return
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
async with self.sys_websession_ssl.post(
f"{self.api_url}/auth/token",
timeout=30,
data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
},
) as resp:
if resp.status != 200:
_LOGGER.error("Can't update Home Assistant access token!")
raise HomeAssistantAuthError()
_LOGGER.info("Updated Home Assistant API token")
tokens = await resp.json()
self.access_token = tokens["access_token"]
self._access_token_expires = datetime.utcnow() + timedelta(
seconds=tokens["expires_in"]
)
@asynccontextmanager
async def make_request(
self,
method: str,
path: str,
json: Optional[Dict[str, Any]] = None,
content_type: Optional[str] = None,
data: Any = None,
timeout: int = 30,
params: Optional[Dict[str, str]] = None,
headers: Optional[Dict[str, str]] = None,
) -> AsyncContextManager[aiohttp.ClientResponse]:
"""Async context manager to make a request with right auth."""
url = f"{self.api_url}/{path}"
headers = headers or {}
# Passthrough content type
if content_type is not None:
headers[hdrs.CONTENT_TYPE] = content_type
for _ in (1, 2):
# Prepare Access token
if self.refresh_token:
await self.ensure_access_token()
headers[hdrs.AUTHORIZATION] = f"Bearer {self.access_token}"
try:
async with getattr(self.sys_websession_ssl, method)(
url,
data=data,
timeout=timeout,
json=json,
headers=headers,
params=params,
) as resp:
# Access token expired
if resp.status == 401 and self.refresh_token:
self.access_token = None
continue
yield resp
return
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error on call %s: %s", url, err)
break
raise HomeAssistantAPIError()
async def check_api_state(self) -> bool:
"""Return True if Home Assistant up and running."""
# Check if port is up
if not await self.sys_run_in_executor(
check_port, self.ip_address, self.api_port
):
return False
# Check if API is up
with suppress(HomeAssistantAPIError):
async with self.make_request("get", "api/config") as resp:
if resp.status in (200, 201):
data = await resp.json()
if data.get("state", "RUNNING") == "RUNNING":
return True
else:
_LOGGER.debug("Home Assistant API return: %d", resp.status)
return False
async def _block_till_run(self, version: str) -> None:
"""Block until Home-Assistant is booting up or startup timeout."""
# Skip landingpage
@ -631,7 +356,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
break
# 2: Check if API response
if await self.check_api_state():
if await self.sys_homeassistant.api.check_api_state():
_LOGGER.info("Detect a running Home Assistant instance")
self._error_state = False
return
@ -659,7 +384,10 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.info("Home Assistant pip installation done")
# 5: Timeout
if timeout and time.monotonic() - start_time > self.wait_boot:
if (
timeout
and time.monotonic() - start_time > self.sys_homeassistant.wait_boot
):
_LOGGER.warning("Don't wait anymore on Home Assistant startup!")
break
@ -671,32 +399,13 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
if await self.instance.exists():
return
_LOGGER.info("Repair Home Assistant %s", self.version)
_LOGGER.info("Repair Home Assistant %s", self.sys_homeassistant.version)
await self.sys_run_in_executor(
self.sys_docker.network.stale_cleanup, self.instance.name
)
# Pull image
try:
await self.instance.install(self.version)
await self.instance.install(self.sys_homeassistant.version)
except DockerAPIError:
_LOGGER.error("Repairing of Home Assistant fails")
def write_pulse(self):
"""Write asound config to file and return True on success."""
pulse_config = self.sys_plugins.audio.pulse_client(
input_profile=self.audio_input, output_profile=self.audio_output
)
# Cleanup wrong maps
if self.path_pulse.is_dir():
shutil.rmtree(self.path_pulse, ignore_errors=True)
# Write pulse config
try:
with self.path_pulse.open("w") as config_file:
config_file.write(pulse_config)
except OSError as err:
_LOGGER.error("Home Assistant can't write pulse/client.config: %s", err)
else:
_LOGGER.info("Update pulse/client.config: %s", self.path_pulse)

View File

@ -12,7 +12,7 @@ from ..utils import AsyncThrottle
_LOGGER: logging.Logger = logging.getLogger(__name__)
class SecretsManager(CoreSysAttributes):
class HomeAssistantSecrets(CoreSysAttributes):
"""Manage Home Assistant secrets."""
def __init__(self, coresys: CoreSys):

View File

@ -156,13 +156,13 @@ class Ingress(JsonConfig, CoreSysAttributes):
async def update_hass_panel(self, addon: Addon):
"""Return True if Home Assistant up and running."""
if not await self.sys_homeassistant.is_running():
if not await self.sys_homeassistant.core.is_running():
_LOGGER.debug("Ignore panel update on Core")
return
# Update UI
method = "post" if addon.ingress_panel else "delete"
async with self.sys_homeassistant.make_request(
async with self.sys_homeassistant.api.make_request(
method, f"api/hassio_push/panel/{addon.slug}"
) as resp:
if resp.status in (200, 201):

View File

@ -169,7 +169,7 @@ class Tasks(CoreSysAttributes):
"""Check running state of Docker and start if they is close."""
# if Home Assistant is active
if (
not await self.sys_homeassistant.is_fails()
not await self.sys_homeassistant.core.is_fails()
or not self.sys_homeassistant.watchdog
or self.sys_homeassistant.error_state
):
@ -177,14 +177,14 @@ class Tasks(CoreSysAttributes):
# if Home Assistant is running
if (
self.sys_homeassistant.in_progress
or await self.sys_homeassistant.is_running()
self.sys_homeassistant.core.in_progress
or await self.sys_homeassistant.core.is_running()
):
return
_LOGGER.warning("Watchdog found a problem with Home Assistant Docker!")
try:
await self.sys_homeassistant.start()
await self.sys_homeassistant.core.start()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
@ -196,7 +196,7 @@ class Tasks(CoreSysAttributes):
"""
# If Home-Assistant is active
if (
not await self.sys_homeassistant.is_fails()
not await self.sys_homeassistant.core.is_fails()
or not self.sys_homeassistant.watchdog
or self.sys_homeassistant.error_state
):
@ -207,8 +207,8 @@ class Tasks(CoreSysAttributes):
# If Home-Assistant API is up
if (
self.sys_homeassistant.in_progress
or await self.sys_homeassistant.check_api_state()
self.sys_homeassistant.core.in_progress
or await self.sys_homeassistant.api.check_api_state()
):
return
@ -221,7 +221,7 @@ class Tasks(CoreSysAttributes):
_LOGGER.error("Watchdog found a problem with Home Assistant API!")
try:
await self.sys_homeassistant.restart()
await self.sys_homeassistant.core.restart()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
finally:

View File

@ -231,7 +231,7 @@ class SnapshotManager(CoreSysAttributes):
_LOGGER.info("Restore %s run Home-Assistant", snapshot.slug)
snapshot.restore_homeassistant()
task_hass = self.sys_create_task(
self.sys_homeassistant.update(snapshot.homeassistant_version)
self.sys_homeassistant.core.update(snapshot.homeassistant_version)
)
# Restore repositories
@ -295,7 +295,7 @@ class SnapshotManager(CoreSysAttributes):
async with snapshot:
# Stop Home-Assistant for config restore
if FOLDER_HOMEASSISTANT in folders:
await self.sys_homeassistant.stop()
await self.sys_homeassistant.core.stop()
snapshot.restore_homeassistant()
# Process folders
@ -308,7 +308,9 @@ class SnapshotManager(CoreSysAttributes):
if homeassistant:
_LOGGER.info("Restore %s run Home-Assistant", snapshot.slug)
task_hass = self.sys_create_task(
self.sys_homeassistant.update(snapshot.homeassistant_version)
self.sys_homeassistant.core.update(
snapshot.homeassistant_version
)
)
if addons:
@ -324,13 +326,13 @@ class SnapshotManager(CoreSysAttributes):
await task_hass
# Do we need start HomeAssistant?
if not await self.sys_homeassistant.is_running():
if not await self.sys_homeassistant.core.is_running():
await self.sys_homeassistant.start()
# Check If we can access to API / otherwise restart
if not await self.sys_homeassistant.check_api_state():
if not await self.sys_homeassistant.api.check_api_state():
_LOGGER.warning("Need restart HomeAssistant for API")
await self.sys_homeassistant.restart()
await self.sys_homeassistant.core.restart()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", snapshot.slug)