Files
hasaddon/hassio-google-drive-backup/backup/ha/harequests.py
T
admins 702d080a1e New Addon
Google BK
2023-04-23 18:17:03 +07:00

330 lines
14 KiB
Python

import os
from typing import Any, Dict
from aiohttp import ClientSession, ClientTimeout
from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError
from injector import inject
from asyncio.exceptions import TimeoutError
from ..util import AsyncHttpGetter
from ..config import Config, Setting, Version
from ..exceptions import HomeAssistantDeleteError, SupervisorConnectionError, SupervisorPermissionError, SupervisorTimeoutError, SupervisorUnexpectedError
from ..model import HABackup
from ..logger import getLogger
from ..util import DataCache
from backup.time import Time
from backup.const import NECESSARY_OLD_BACKUP_PLURAL_NAME, NECESSARY_OLD_SUPERVISOR_URL
from yarl import URL
logger = getLogger(__name__)
HEADER_TOKEN = "X-Supervisor-Token"
NOTIFICATION_ID = "backup_broken"
EVENT_BACKUP_START = "backup_started"
EVENT_BACKUP_END = "backup_ended"
VERSION_BACKUP_PATH = Version.parse("2021.8")
def supervisor_call(func):
async def wrap_and_call(*args, **kwargs):
try:
return await func(*args, **kwargs)
except ClientConnectorError:
raise SupervisorConnectionError()
except TimeoutError:
raise SupervisorConnectionError()
except ClientResponseError as e:
if e.status == 403:
raise SupervisorPermissionError()
raise
return wrap_and_call
class HaRequests():
"""
Stores logic for interacting with the supervisor add-on API
"""
@inject
def __init__(self, config: Config, session: ClientSession, time: Time, data_cache: DataCache):
self.config: Config = config
self.cache = {}
self.session = session
self._time = time
self._data_cache = data_cache
# default the supervisor versio to using the "most featured" when it can't be parsed.
self._super_version = VERSION_BACKUP_PATH
def getSupervisorURL(self) -> URL:
if len(self.config.get(Setting.SUPERVISOR_URL)) > 0:
return URL(self.config.get(Setting.SUPERVISOR_URL))
if 'SUPERVISOR_TOKEN' in os.environ:
return URL("http://supervisor")
else:
return URL(NECESSARY_OLD_SUPERVISOR_URL)
def _getBackupPath(self):
if self.supportsBackupPaths():
return "backups"
return NECESSARY_OLD_BACKUP_PLURAL_NAME
def supportsBackupPaths(self):
return not self._super_version or self._super_version >= VERSION_BACKUP_PATH
@supervisor_call
async def createBackup(self, info):
if 'folders' in info or 'addons' in info:
url = self.getSupervisorURL().with_path("{0}/new/partial".format(self._getBackupPath()))
else:
url = self.getSupervisorURL().with_path("{0}/new/full".format(self._getBackupPath()))
return await self._postHassioData(url, info, timeout=ClientTimeout(total=self.config.get(Setting.PENDING_BACKUP_TIMEOUT_SECONDS)))
@supervisor_call
async def auth(self, user: str, password: str) -> None:
await self._postHassioData(self.getSupervisorURL().with_path("auth"), {"username": user, "password": password}, headers=self._altAuthHeaders())
@supervisor_call
async def upload(self, stream):
url = self.getSupervisorURL().with_path("{0}/new/upload".format(self._getBackupPath()))
return await self._postHassioData(url, data=stream)
@supervisor_call
async def delete(self, slug) -> None:
if slug in self.cache:
del self.cache[slug]
try:
if self.supportsBackupPaths():
delete_url = self.getSupervisorURL().with_path("{1}/{0}".format(slug, self._getBackupPath()))
await self._sendHassioData("delete", delete_url, {})
else:
delete_url = self.getSupervisorURL().with_path("{1}/{0}/remove".format(slug, self._getBackupPath()))
await self._sendHassioData("post", delete_url, {})
except ClientResponseError as e:
if e.status == 400:
raise HomeAssistantDeleteError()
raise e
@supervisor_call
async def startAddon(self, slug) -> None:
url = self.getSupervisorURL().with_path("addons/{0}/start".format(slug))
await self._postHassioData(url, {})
@supervisor_call
async def stopAddon(self, slug) -> None:
url = self.getSupervisorURL().with_path("addons/{0}/stop".format(slug))
await self._postHassioData(url, {})
@supervisor_call
async def backup(self, slug):
if slug in self.cache:
info = self.cache[slug]
else:
info = await self._getHassioData(self.getSupervisorURL().with_path("{1}/{0}/info".format(slug, self._getBackupPath())))
self.cache[slug] = info
return HABackup(info, self._data_cache, self.config, self.config.isRetained(slug))
@supervisor_call
async def backups(self):
return await self._getHassioData(self.getSupervisorURL().with_path(self._getBackupPath()))
@supervisor_call
async def haInfo(self):
return await self._getHassioData(self.getSupervisorURL().with_path("core/info"))
@supervisor_call
async def selfInfo(self) -> Dict[str, Any]:
return await self.getAddonInfo("self")
@supervisor_call
async def getAddonInfo(self, addon_slug) -> Dict[str, Any]:
return await self._getHassioData(self.getSupervisorURL().with_path("addons/{0}/info".format(addon_slug)))
@supervisor_call
async def getAddons(self) -> Dict[str, Any]:
return await self._getHassioData(self.getSupervisorURL().with_path("addons"))
@supervisor_call
async def hassosInfo(self) -> Dict[str, Any]:
return await self._getHassioData(self.getSupervisorURL().with_path("hassos/info"))
@supervisor_call
async def info(self) -> Dict[str, Any]:
return await self._getHassioData(self.getSupervisorURL().with_path("info"))
@supervisor_call
async def refreshBackups(self):
url = self.getSupervisorURL().with_path("{0}/reload".format(self._getBackupPath()))
return await self._postHassioData(url)
@supervisor_call
async def supervisorInfo(self):
url = self.getSupervisorURL().with_path("supervisor/info")
info = await self._getHassioData(url)
# parse the supervisor version
if 'version' in info:
self._super_version = Version.parse(info['version'])
return info
@supervisor_call
async def restore(self, slug: str, password: str = None) -> None:
url = self.getSupervisorURL().with_path("{1}/{0}/restore/full".format(slug, self._getBackupPath()))
if password:
await self._postHassioData(url, {'password': password})
else:
await self._postHassioData(url, {})
@supervisor_call
async def download(self, slug) -> AsyncHttpGetter:
url = self.getSupervisorURL().with_path("{1}/{0}/download".format(slug, self._getBackupPath()))
ret = AsyncHttpGetter(url,
self._getAuthHeaders(),
self.session,
timeoutFactory=SupervisorTimeoutError.factory,
otherErrorFactory=SupervisorUnexpectedError.factory,
timeout=ClientTimeout(sock_connect=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS),
sock_read=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS)),
time=self._time)
return ret
@supervisor_call
async def getSuperLogs(self):
url = self.getSupervisorURL().with_path("supervisor/logs")
async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
resp.raise_for_status()
return await resp.text()
@supervisor_call
async def getCoreLogs(self):
url = self.getSupervisorURL().with_path("core/logs")
async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
resp.raise_for_status()
return await resp.text()
async def _validateHassioReply(self, resp) -> Dict[str, Any]:
async with resp:
resp.raise_for_status()
details: Dict[str, Any] = await resp.json()
if "result" not in details or details["result"] != "ok":
if "result" in details:
raise Exception("Hassio said: " + details["result"])
else:
raise Exception(
"Malformed response from Hassio: " + str(details))
if "data" not in details:
return {}
if self.config.get(Setting.TRACE_REQUESTS):
logger.trace("Hassio replied: %s", details)
return details["data"]
async def getAddonLogo(self, slug: str):
url = self.getSupervisorURL().with_path("addons/{0}/icon".format(slug))
async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
resp.raise_for_status()
return (resp.headers['Content-Type'], await resp.read())
def _getToken(self):
configured = self.config.get(Setting.SUPERVISOR_TOKEN)
if configured and len(configured) > 0:
return configured
if "SUPERVISOR_TOKEN" in os.environ:
return os.environ.get("SUPERVISOR_TOKEN")
# Older versions of the supervisor use a different name for the token.
return os.environ.get("HASSIO_TOKEN")
def _getAuthHeaders(self):
return {
'Authorization': 'Bearer ' + self._getToken()
}
def _altAuthHeaders(self):
return {
HEADER_TOKEN: self._getToken()
}
@supervisor_call
async def _getHassioData(self, url: URL) -> Dict[str, Any]:
if self.config.get(Setting.TRACE_REQUESTS):
logger.trace("Making Hassio request: " + str(url))
return await self._validateHassioReply(await self.session.get(url, headers=self._getAuthHeaders()))
async def _postHassioData(self, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]:
return await self._sendHassioData("post", url, json, file, data, timeout, headers)
@supervisor_call
async def _sendHassioData(self, method: str, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]:
if headers is None:
headers = self._getAuthHeaders()
if self.config.get(Setting.TRACE_REQUESTS):
logger.trace("Making Hassio request: " + str(url))
return await self._validateHassioReply(await self.session.request(method, url, headers=headers, json=json, data=data, timeout=timeout))
async def _postHaData(self, path: str, data: Dict[str, Any]) -> None:
url = self.getSupervisorURL().with_path("/core/api/" + path)
async with self.session.post(url, headers=self._getAuthHeaders(), json=data) as resp:
resp.raise_for_status()
async def sendNotification(self, title: str, message: str) -> None:
data: Dict[str, str] = {
"title": title,
"message": message,
"notification_id": NOTIFICATION_ID
}
await self._postHaData("services/persistent_notification/create", data)
async def eventBackupStart(self, name, time):
await self._sendEvent(EVENT_BACKUP_START, {
'backup_name': name,
'backup_time': str(time)
})
async def eventBackupEnd(self, name, time, completed):
await self._sendEvent(EVENT_BACKUP_END, {
'completed': completed,
'backup_name': name,
'backup_time': str(time)
})
async def _sendEvent(self, event_name: str, data: Dict[str, str]) -> None:
await self._postHaData("events/" + event_name, data)
async def dismissNotification(self) -> None:
data: Dict[str, str] = {
"notification_id": NOTIFICATION_ID
}
await self._postHaData("services/persistent_notification/dismiss", data)
async def updateBackupStaleSensor(self, state: bool) -> None:
if self.config.get(Setting.CALL_BACKUP_SNAPSHOT):
data: Dict[str, Any] = {
"state": state,
"attributes": {
"friendly_name": "Snapshots Stale",
"device_class": "problem"
}
}
await self._postHaData("states/binary_sensor." + NECESSARY_OLD_BACKUP_PLURAL_NAME + "_stale", data)
else:
data: Dict[str, Any] = {
"state": state,
"attributes": {
"friendly_name": "Backups Stale",
"device_class": "problem"
}
}
await self._postHaData("states/binary_sensor.backups_stale", data)
@supervisor_call
async def updateConfig(self, config) -> None:
return await self._postHassioData(self.getSupervisorURL().with_path("addons/self/options"), {'options': config})
@supervisor_call
async def updateAddonOptions(self, slug, options):
return await self._postHassioData(self.getSupervisorURL().with_path("addons/{0}/options".format(slug)), options)
async def updateEntity(self, entity, data):
await self._postHaData("states/" + entity, data)