Upgrade HACS
This commit is contained in:
@@ -217,6 +217,13 @@ class HacsRepositories:
|
||||
"""Return a list of downloaded repositories."""
|
||||
return [repo for repo in self._repositories if repo.data.installed]
|
||||
|
||||
def category_downloaded(self, category: HacsCategory) -> bool:
|
||||
"""Check if a given category has been downloaded."""
|
||||
for repository in self.list_downloaded:
|
||||
if repository.data.category == category:
|
||||
return True
|
||||
return False
|
||||
|
||||
def register(self, repository: HacsRepository, default: bool = False) -> None:
|
||||
"""Register a repository."""
|
||||
repo_id = str(repository.data.id)
|
||||
@@ -368,7 +375,7 @@ class HacsBase:
|
||||
status = HacsStatus()
|
||||
system = HacsSystem()
|
||||
validation: ValidationManager | None = None
|
||||
version: str | None = None
|
||||
version: AwesomeVersion | None = None
|
||||
|
||||
@property
|
||||
def integration_dir(self) -> pathlib.Path:
|
||||
@@ -592,7 +599,7 @@ class HacsBase:
|
||||
repository.data.id = repository_id
|
||||
|
||||
else:
|
||||
if self.hass is not None and ((check and repository.data.new) or self.status.new):
|
||||
if self.hass is not None and check and repository.data.new:
|
||||
self.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
@@ -689,15 +696,23 @@ class HacsBase:
|
||||
|
||||
self.async_dispatch(HacsDispatchEvent.STATUS, {})
|
||||
|
||||
async def async_download_file(self, url: str, *, headers: dict | None = None) -> bytes | None:
|
||||
async def async_download_file(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict | None = None,
|
||||
keep_url: bool = False,
|
||||
nolog: bool = False,
|
||||
**_,
|
||||
) -> bytes | None:
|
||||
"""Download files, and return the content."""
|
||||
if url is None:
|
||||
return None
|
||||
|
||||
if "tags/" in url:
|
||||
if not keep_url and "tags/" in url:
|
||||
url = url.replace("tags/", "")
|
||||
|
||||
self.log.debug("Downloading %s", url)
|
||||
self.log.debug("Trying to download %s", url)
|
||||
timeouts = 0
|
||||
|
||||
while timeouts < 5:
|
||||
@@ -733,6 +748,7 @@ class HacsBase:
|
||||
except (
|
||||
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
) as exception:
|
||||
if not nolog:
|
||||
self.log.exception("Download failed - %s", exception)
|
||||
|
||||
return None
|
||||
@@ -763,24 +779,24 @@ class HacsBase:
|
||||
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
|
||||
self.enable_hacs_category(HacsCategory(category))
|
||||
|
||||
if self.configuration.experimental and self.core.ha_version >= "2023.4.0b0":
|
||||
if self.configuration.experimental:
|
||||
self.enable_hacs_category(HacsCategory.TEMPLATE)
|
||||
|
||||
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components:
|
||||
if (
|
||||
HacsCategory.PYTHON_SCRIPT in self.hass.config.components
|
||||
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
|
||||
):
|
||||
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
|
||||
|
||||
if self.hass.services.has_service("frontend", "reload_themes"):
|
||||
if self.hass.services.has_service(
|
||||
"frontend", "reload_themes"
|
||||
) or self.repositories.category_downloaded(HacsCategory.THEME):
|
||||
self.enable_hacs_category(HacsCategory.THEME)
|
||||
|
||||
if self.configuration.appdaemon:
|
||||
self.enable_hacs_category(HacsCategory.APPDAEMON)
|
||||
if self.configuration.netdaemon:
|
||||
downloaded_netdaemon = [
|
||||
x
|
||||
for x in self.repositories.list_downloaded
|
||||
if x.data.category == HacsCategory.NETDAEMON
|
||||
]
|
||||
if len(downloaded_netdaemon) != 0:
|
||||
if self.repositories.category_downloaded(HacsCategory.NETDAEMON):
|
||||
self.log.warning(
|
||||
"NetDaemon in HACS is deprectaded. It will stop working in the future. "
|
||||
"Please remove all your current NetDaemon repositories from HACS "
|
||||
@@ -871,15 +887,6 @@ class HacsBase:
|
||||
repository.repository_manifest.update_data(
|
||||
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest}
|
||||
)
|
||||
self.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": repository.data.full_name,
|
||||
"repository_id": repository.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
if category == "integration":
|
||||
self.status.inital_fetch_done = True
|
||||
@@ -896,6 +903,8 @@ class HacsBase:
|
||||
)
|
||||
self.repositories.unregister(repository)
|
||||
|
||||
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
|
||||
|
||||
async def async_get_category_repositories(self, category: HacsCategory) -> None:
|
||||
"""Get repositories from category."""
|
||||
if self.system.disabled:
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
"""Adds config flow for HACS."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogithubapi import GitHubDeviceAPI, GitHubException
|
||||
from aiogithubapi import (
|
||||
GitHubDeviceAPI,
|
||||
GitHubException,
|
||||
GitHubLoginDeviceModel,
|
||||
GitHubLoginOauthModel,
|
||||
)
|
||||
from aiogithubapi.common.const import OAUTH_USER_LOGIN
|
||||
from awesomeversion import AwesomeVersion
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlow, OptionsFlow
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import UnknownFlow
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.loader import async_get_integration
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -33,23 +40,22 @@ if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for HACS."""
|
||||
|
||||
hass: HomeAssistant
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
hass: HomeAssistant
|
||||
activation_task: asyncio.Task | None = None
|
||||
device: GitHubDeviceAPI | None = None
|
||||
|
||||
_registration: GitHubLoginDeviceModel | None = None
|
||||
_activation: GitHubLoginOauthModel | None = None
|
||||
_reauth: bool = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
self.device = None
|
||||
self.activation = None
|
||||
self.log = LOGGER
|
||||
self._progress_task = None
|
||||
self._login_device = None
|
||||
self._reauth = False
|
||||
self._user_input = {}
|
||||
|
||||
async def async_step_user(self, user_input):
|
||||
@@ -72,45 +78,58 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
## Initial form
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
@callback
|
||||
def async_remove(self):
|
||||
"""Cleanup."""
|
||||
if self.activation_task and not self.activation_task.done():
|
||||
self.activation_task.cancel()
|
||||
|
||||
async def async_step_device(self, _user_input):
|
||||
"""Handle device steps"""
|
||||
"""Handle device steps."""
|
||||
|
||||
async def _wait_for_activation(_=None):
|
||||
if self._login_device is None or self._login_device.expires_in is None:
|
||||
async_call_later(self.hass, 1, _wait_for_activation)
|
||||
return
|
||||
async def _wait_for_activation() -> None:
|
||||
try:
|
||||
response = await self.device.activation(device_code=self._registration.device_code)
|
||||
self._activation = response.data
|
||||
finally:
|
||||
|
||||
response = await self.device.activation(device_code=self._login_device.device_code)
|
||||
self.activation = response.data
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
|
||||
)
|
||||
async def _progress():
|
||||
with suppress(UnknownFlow):
|
||||
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
|
||||
|
||||
self.hass.async_create_task(_progress())
|
||||
|
||||
if not self.activation:
|
||||
integration = await async_get_integration(self.hass, DOMAIN)
|
||||
if not self.device:
|
||||
integration = await async_get_integration(self.hass, DOMAIN)
|
||||
self.device = GitHubDeviceAPI(
|
||||
client_id=CLIENT_ID,
|
||||
session=aiohttp_client.async_get_clientsession(self.hass),
|
||||
**{"client_name": f"HACS/{integration.version}"},
|
||||
)
|
||||
async_call_later(self.hass, 1, _wait_for_activation)
|
||||
try:
|
||||
response = await self.device.register()
|
||||
self._login_device = response.data
|
||||
self._registration = response.data
|
||||
except GitHubException as exception:
|
||||
LOGGER.exception(exception)
|
||||
return self.async_abort(reason="could_not_register")
|
||||
|
||||
if self.activation_task is None:
|
||||
self.activation_task = self.hass.async_create_task(_wait_for_activation())
|
||||
|
||||
if self.activation_task.done():
|
||||
if (exception := self.activation_task.exception()) is not None:
|
||||
LOGGER.exception(exception)
|
||||
return self.async_show_progress_done(next_step_id="could_not_register")
|
||||
return self.async_show_progress_done(next_step_id="device_done")
|
||||
|
||||
return self.async_show_progress(
|
||||
step_id="device",
|
||||
progress_action="wait_for_device",
|
||||
description_placeholders={
|
||||
"url": OAUTH_USER_LOGIN,
|
||||
"code": self._login_device.user_code,
|
||||
"code": self._registration.user_code,
|
||||
},
|
||||
)
|
||||
except GitHubException as exception:
|
||||
self.log.error(exception)
|
||||
return self.async_abort(reason="github")
|
||||
|
||||
return self.async_show_progress_done(next_step_id="device_done")
|
||||
|
||||
async def _show_config_form(self, user_input):
|
||||
"""Show the configuration form to edit location data."""
|
||||
@@ -146,7 +165,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self._reauth:
|
||||
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data={**existing_entry.data, "token": self.activation.access_token}
|
||||
existing_entry, data={**existing_entry.data, "token": self._activation.access_token}
|
||||
)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
@@ -154,13 +173,17 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
"token": self.activation.access_token,
|
||||
"token": self._activation.access_token,
|
||||
},
|
||||
options={
|
||||
"experimental": self._user_input.get("experimental", False),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_could_not_register(self, _user_input=None):
|
||||
"""Handle issues that need transition await from progress step."""
|
||||
return self.async_abort(reason="could_not_register")
|
||||
|
||||
async def async_step_reauth(self, _user_input=None):
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
@@ -181,7 +204,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return HacsOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
class HacsOptionsFlowHandler(OptionsFlow):
|
||||
"""HACS config flow options handler."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
|
||||
@@ -13,6 +13,18 @@ from .hacs_frontend_experimental import (
|
||||
locate_dir as experimental_locate_dir,
|
||||
)
|
||||
|
||||
try:
|
||||
from homeassistant.components.frontend import add_extra_js_url
|
||||
except ImportError:
|
||||
|
||||
def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None:
|
||||
hacs: HacsBase = hass.data.get(DOMAIN)
|
||||
hacs.log.error("Could not import add_extra_js_url from frontend.")
|
||||
if "frontend_extra_module_url" not in hass.data:
|
||||
hass.data["frontend_extra_module_url"] = set()
|
||||
hass.data["frontend_extra_module_url"].add(url)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import HacsBase
|
||||
|
||||
@@ -45,9 +57,7 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
|
||||
hass.http.register_static_path(
|
||||
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
|
||||
)
|
||||
if "frontend_extra_module_url" not in hass.data:
|
||||
hass.data["frontend_extra_module_url"] = set()
|
||||
hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
|
||||
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
|
||||
|
||||
hacs.frontend_version = (
|
||||
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"requirements": [
|
||||
"aiogithubapi>=22.10.1"
|
||||
],
|
||||
"version": "1.33.0"
|
||||
"version": "1.34.0"
|
||||
}
|
||||
@@ -15,7 +15,6 @@ from aiogithubapi import (
|
||||
AIOGitHubAPINotModifiedException,
|
||||
GitHubReleaseModel,
|
||||
)
|
||||
from aiogithubapi.const import BASE_API_URL
|
||||
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
|
||||
import attr
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
@@ -28,6 +27,7 @@ from ..exceptions import (
|
||||
HacsRepositoryArchivedException,
|
||||
HacsRepositoryExistException,
|
||||
)
|
||||
from ..types import DownloadableContent
|
||||
from ..utils.backup import Backup, BackupNetDaemon
|
||||
from ..utils.decode import decode_content
|
||||
from ..utils.decorator import concurrent
|
||||
@@ -38,6 +38,7 @@ from ..utils.path import is_safe
|
||||
from ..utils.queue_manager import QueueManager
|
||||
from ..utils.store import async_remove_store
|
||||
from ..utils.template import render_template
|
||||
from ..utils.url import github_archive, github_release_asset
|
||||
from ..utils.validate import Validate
|
||||
from ..utils.version import (
|
||||
version_left_higher_or_equal_then_right,
|
||||
@@ -558,40 +559,37 @@ class HacsRepository:
|
||||
|
||||
return True
|
||||
|
||||
async def download_zip_files(self, validate) -> None:
|
||||
async def download_zip_files(self, validate: Validate) -> None:
|
||||
"""Download ZIP archive from repository release."""
|
||||
try:
|
||||
contents = None
|
||||
target_ref = self.ref.split("/")[1]
|
||||
|
||||
for release in self.releases.objects:
|
||||
self.logger.debug(
|
||||
"%s ref: %s --- tag: %s", self.string, target_ref, release.tag_name
|
||||
try:
|
||||
await self.async_download_zip_file(
|
||||
DownloadableContent(
|
||||
name=self.repository_manifest.filename,
|
||||
url=github_release_asset(
|
||||
repository=self.data.full_name,
|
||||
version=self.ref,
|
||||
filename=self.repository_manifest.filename,
|
||||
),
|
||||
),
|
||||
validate,
|
||||
)
|
||||
if release.tag_name == target_ref:
|
||||
contents = release.assets
|
||||
break
|
||||
|
||||
if not contents:
|
||||
validate.errors.append(f"No assets found for release '{self.ref}'")
|
||||
return
|
||||
|
||||
download_queue = QueueManager(hass=self.hacs.hass)
|
||||
|
||||
for content in contents or []:
|
||||
download_queue.add(self.async_download_zip_file(content, validate))
|
||||
|
||||
await download_queue.execute()
|
||||
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
validate.errors.append("Download was not completed")
|
||||
validate.errors.append(
|
||||
f"Download of {self.repository_manifest.filename} was not completed"
|
||||
)
|
||||
|
||||
async def async_download_zip_file(self, content, validate) -> None:
|
||||
async def async_download_zip_file(
|
||||
self,
|
||||
content: DownloadableContent,
|
||||
validate: Validate,
|
||||
) -> None:
|
||||
"""Download ZIP archive from repository release."""
|
||||
try:
|
||||
filecontent = await self.hacs.async_download_file(content.browser_download_url)
|
||||
filecontent = await self.hacs.async_download_file(content["url"])
|
||||
|
||||
if filecontent is None:
|
||||
validate.errors.append(f"[{content.name}] was not downloaded")
|
||||
validate.errors.append(f"Failed to download {content['url']}")
|
||||
return
|
||||
|
||||
temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp)
|
||||
@@ -608,16 +606,17 @@ class HacsRepository:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
if result:
|
||||
self.logger.info("%s Download of %s completed", self.string, content.name)
|
||||
self.logger.info("%s Download of %s completed", self.string, content["name"])
|
||||
await self.hacs.hass.async_add_executor_job(cleanup_temp_dir)
|
||||
return
|
||||
|
||||
validate.errors.append(f"[{content.name}] was not downloaded")
|
||||
validate.errors.append(f"[{content['name']}] was not downloaded")
|
||||
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
validate.errors.append("Download was not completed")
|
||||
|
||||
async def download_content(self) -> None:
|
||||
async def download_content(self, version: string | None = None) -> None:
|
||||
"""Download the content of a directory."""
|
||||
contents: list[FileInformation] | None = None
|
||||
if self.hacs.configuration.experimental:
|
||||
if (
|
||||
not self.repository_manifest.zip_release
|
||||
@@ -631,9 +630,15 @@ class HacsRepository:
|
||||
except HacsException as exception:
|
||||
self.logger.exception(exception)
|
||||
|
||||
contents = self.gather_files_to_download()
|
||||
if self.repository_manifest.filename:
|
||||
self.logger.debug("%s %s", self.string, self.repository_manifest.filename)
|
||||
|
||||
if self.content.path.remote == "release" and version is not None:
|
||||
contents = await self.release_contents(version)
|
||||
|
||||
if not contents:
|
||||
contents = self.gather_files_to_download()
|
||||
|
||||
if not contents:
|
||||
raise HacsException("No content to download")
|
||||
|
||||
@@ -654,14 +659,16 @@ class HacsRepository:
|
||||
if not ref:
|
||||
raise HacsException("Missing required elements.")
|
||||
|
||||
url = f"{BASE_API_URL}/repos/{self.data.full_name}/zipball/{ref}"
|
||||
|
||||
filecontent = await self.hacs.async_download_file(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"token {self.hacs.configuration.token}",
|
||||
"User-Agent": f"HACS/{self.hacs.version}",
|
||||
},
|
||||
github_archive(repository=self.data.full_name, version=ref, variant="tags"),
|
||||
keep_url=True,
|
||||
nolog=True,
|
||||
)
|
||||
|
||||
if filecontent is None:
|
||||
filecontent = await self.hacs.async_download_file(
|
||||
github_archive(repository=self.data.full_name, version=ref, variant="heads"),
|
||||
keep_url=True,
|
||||
)
|
||||
if filecontent is None:
|
||||
raise HacsException(f"[{self}] Failed to download zipball")
|
||||
@@ -681,8 +688,13 @@ class HacsRepository:
|
||||
and filename != self.content.path.remote
|
||||
):
|
||||
path.filename = filename.replace(self.content.path.remote, "")
|
||||
if path.filename == "/":
|
||||
# Blank files is not valid, and will start to throw in Python 3.12
|
||||
continue
|
||||
extractable.append(path)
|
||||
|
||||
if len(extractable) == 0:
|
||||
raise HacsException("No content to extract")
|
||||
zip_file.extractall(self.content.path.local, extractable)
|
||||
|
||||
def cleanup_temp_dir():
|
||||
@@ -732,25 +744,7 @@ class HacsRepository:
|
||||
if not info_files:
|
||||
return ""
|
||||
|
||||
try:
|
||||
response = await self.hacs.async_github_api_method(
|
||||
method=self.hacs.githubapi.repos.contents.get,
|
||||
raise_exception=False,
|
||||
repository=self.data.full_name,
|
||||
path=info_files[0],
|
||||
)
|
||||
if response:
|
||||
return render_template(
|
||||
self.hacs,
|
||||
decode_content(response.data.content)
|
||||
.replace("<svg", "<disabled")
|
||||
.replace("</svg", "</disabled"),
|
||||
self,
|
||||
)
|
||||
except BaseException as exc: # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
self.logger.error("%s %s", self.string, exc)
|
||||
|
||||
return ""
|
||||
return await self.get_documentation(filename=info_files[0]) or ""
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Run remove tasks."""
|
||||
@@ -799,7 +793,7 @@ class HacsRepository:
|
||||
|
||||
try:
|
||||
if self.data.category == "python_script":
|
||||
local_path = f"{self.content.path.local}/{self.data.name}.py"
|
||||
local_path = f"{self.content.path.local}/{self.data.file_name}"
|
||||
elif self.data.category == "template":
|
||||
local_path = f"{self.content.path.local}/{self.data.file_name}"
|
||||
elif self.data.category == "theme":
|
||||
@@ -888,7 +882,7 @@ class HacsRepository:
|
||||
await self.async_pre_install()
|
||||
self.logger.info("%s Pre installation steps completed", self.string)
|
||||
|
||||
async def async_install(self) -> None:
|
||||
async def async_install(self, *, version: str | None = None, **_) -> None:
|
||||
"""Run install steps."""
|
||||
await self._async_pre_install()
|
||||
self.hacs.async_dispatch(
|
||||
@@ -896,7 +890,7 @@ class HacsRepository:
|
||||
{"repository": self.data.full_name, "progress": 30},
|
||||
)
|
||||
self.logger.info("%s Running installation steps", self.string)
|
||||
await self.async_install_repository()
|
||||
await self.async_install_repository(version=version)
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
|
||||
{"repository": self.data.full_name, "progress": 90},
|
||||
@@ -927,10 +921,10 @@ class HacsRepository:
|
||||
)
|
||||
self.logger.info("%s Post installation steps completed", self.string)
|
||||
|
||||
async def async_install_repository(self) -> None:
|
||||
async def async_install_repository(self, *, version: str | None = None, **_) -> None:
|
||||
"""Common installation steps of the repository."""
|
||||
persistent_directory = None
|
||||
await self.update_repository(force=True)
|
||||
await self.update_repository(force=version is None)
|
||||
if self.content.path.local is None:
|
||||
raise HacsException("repository.content.path.local is None")
|
||||
self.validate.errors.clear()
|
||||
@@ -938,11 +932,11 @@ class HacsRepository:
|
||||
if not self.can_download:
|
||||
raise HacsException("The version of Home Assistant is not compatible with this version")
|
||||
|
||||
version = self.version_to_download()
|
||||
if version == self.data.default_branch:
|
||||
self.ref = version
|
||||
version_to_install = version or self.version_to_download()
|
||||
if version_to_install == self.data.default_branch:
|
||||
self.ref = version_to_install
|
||||
else:
|
||||
self.ref = f"tags/{version}"
|
||||
self.ref = f"tags/{version_to_install}"
|
||||
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
|
||||
@@ -970,16 +964,17 @@ class HacsRepository:
|
||||
|
||||
self.hacs.log.debug("%s Local path is set to %s", self.string, self.content.path.local)
|
||||
self.hacs.log.debug("%s Remote path is set to %s", self.string, self.content.path.remote)
|
||||
self.hacs.log.debug("%s Version to install: %s", self.string, version_to_install)
|
||||
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
|
||||
{"repository": self.data.full_name, "progress": 50},
|
||||
)
|
||||
|
||||
if self.repository_manifest.zip_release and version != self.data.default_branch:
|
||||
if self.repository_manifest.zip_release and self.repository_manifest.filename:
|
||||
await self.download_zip_files(self.validate)
|
||||
else:
|
||||
await self.download_content()
|
||||
await self.download_content(version_to_install)
|
||||
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
|
||||
@@ -1010,10 +1005,10 @@ class HacsRepository:
|
||||
self.data.installed = True
|
||||
self.data.installed_commit = self.data.last_commit
|
||||
|
||||
if version == self.data.default_branch:
|
||||
if version_to_install == self.data.default_branch:
|
||||
self.data.installed_version = None
|
||||
else:
|
||||
self.data.installed_version = version
|
||||
self.data.installed_version = version_to_install
|
||||
|
||||
async def async_get_legacy_repository_object(
|
||||
self,
|
||||
@@ -1228,6 +1223,25 @@ class HacsRepository:
|
||||
files.append(FileInformation(path.download_url, path.full_path, path.filename))
|
||||
return files
|
||||
|
||||
async def release_contents(self, version: str | None = None) -> list[FileInformation] | None:
|
||||
"""Gather the contents of a release."""
|
||||
release = await self.hacs.async_github_api_method(
|
||||
method=self.hacs.githubapi.generic,
|
||||
endpoint=f"/repos/{self.data.full_name}/releases/tags/{version}",
|
||||
raise_exception=False,
|
||||
)
|
||||
if release is None:
|
||||
return None
|
||||
|
||||
return [
|
||||
FileInformation(
|
||||
url=asset.get("browser_download_url"),
|
||||
path=asset.get("name"),
|
||||
name=asset.get("name"),
|
||||
)
|
||||
for asset in release.data.get("assets", [])
|
||||
]
|
||||
|
||||
@concurrent(concurrenttasks=10)
|
||||
async def dowload_repository_content(self, content: FileInformation) -> None:
|
||||
"""Download content."""
|
||||
@@ -1303,3 +1317,58 @@ class HacsRepository:
|
||||
return self.data.selected_tag
|
||||
|
||||
return self.data.default_branch or "main"
|
||||
|
||||
async def get_documentation(
|
||||
self,
|
||||
*,
|
||||
filename: str | None = None,
|
||||
**kwargs,
|
||||
) -> str | None:
|
||||
"""Get the documentation of the repository."""
|
||||
if filename is None:
|
||||
return None
|
||||
|
||||
version = (
|
||||
(self.data.installed_version or self.data.installed_commit)
|
||||
if self.data.installed
|
||||
else (self.data.last_version or self.data.last_commit or self.ref)
|
||||
)
|
||||
self.logger.debug(
|
||||
"%s Getting documentation for version=%s,filename=%s",
|
||||
self.string,
|
||||
version,
|
||||
filename,
|
||||
)
|
||||
if version is None:
|
||||
return None
|
||||
|
||||
result = await self.hacs.async_download_file(
|
||||
f"https://raw.githubusercontent.com/{self.data.full_name}/{version}/{filename}",
|
||||
nolog=True,
|
||||
)
|
||||
|
||||
return (
|
||||
render_template(
|
||||
self.hacs,
|
||||
result.decode(encoding="utf-8")
|
||||
.replace("<svg", "<disabled")
|
||||
.replace("</svg", "</disabled"),
|
||||
self,
|
||||
)
|
||||
if result
|
||||
else None
|
||||
)
|
||||
|
||||
async def get_hacs_json(self, *, version: str, **kwargs) -> HacsManifest | None:
|
||||
"""Get the hacs.json file of the repository."""
|
||||
self.logger.debug("%s Getting hacs.json for version=%s", self.string, version)
|
||||
try:
|
||||
result = await self.hacs.async_download_file(
|
||||
f"https://raw.githubusercontent.com/{self.data.full_name}/{version}/hacs.json",
|
||||
nolog=True,
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
return HacsManifest.from_dict(json_loads(result))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return None
|
||||
|
||||
@@ -3,14 +3,16 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
|
||||
from homeassistant.core import HomeAssistantError, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .base import HacsBase
|
||||
from .const import DOMAIN
|
||||
from .entity import HacsRepositoryEntity
|
||||
from .enums import HacsCategory, HacsDispatchEvent
|
||||
from .exceptions import HacsException
|
||||
from .repositories.base import HacsManifest
|
||||
|
||||
|
||||
async def async_setup_entry(hass, _config_entry, async_add_devices):
|
||||
@@ -25,13 +27,12 @@ async def async_setup_entry(hass, _config_entry, async_add_devices):
|
||||
class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
|
||||
"""Update entities for repositories downloaded with HACS."""
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int | None:
|
||||
"""Return the supported features of the entity."""
|
||||
features = 4 | 16
|
||||
if self.repository.can_download:
|
||||
features = features | 1
|
||||
return features
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
| UpdateEntityFeature.SPECIFIC_VERSION
|
||||
| UpdateEntityFeature.PROGRESS
|
||||
| UpdateEntityFeature.RELEASE_NOTES
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
@@ -75,14 +76,59 @@ class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
|
||||
|
||||
return f"https://brands.home-assistant.io/_/{self.repository.data.domain}/icon.png"
|
||||
|
||||
async def _ensure_capabilities(self, version: str | None, **kwargs: Any) -> None:
|
||||
"""Ensure that the entity has capabilities."""
|
||||
target_manifest: HacsManifest | None = None
|
||||
if version is None:
|
||||
if not self.repository.can_download:
|
||||
raise HomeAssistantError(
|
||||
f"This {self.repository.data.category.value} is not available for download."
|
||||
)
|
||||
return
|
||||
|
||||
if version == self.repository.data.last_version:
|
||||
target_manifest = self.repository.repository_manifest
|
||||
else:
|
||||
target_manifest = await self.repository.get_hacs_json(version=version)
|
||||
|
||||
if target_manifest is None:
|
||||
raise HomeAssistantError(
|
||||
f"The version {version} for this {self.repository.data.category.value} can not be used with HACS."
|
||||
)
|
||||
|
||||
if (
|
||||
target_manifest.homeassistant is not None
|
||||
and self.hacs.core.ha_version < target_manifest.homeassistant
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"This version requires Home Assistant {target_manifest.homeassistant} or newer."
|
||||
)
|
||||
if target_manifest.hacs is not None and self.hacs.version < target_manifest.hacs:
|
||||
raise HomeAssistantError(f"This version requires HACS {target_manifest.hacs} or newer.")
|
||||
|
||||
async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
|
||||
"""Install an update."""
|
||||
await self._ensure_capabilities(version)
|
||||
self.repository.logger.info("Starting update, %s", version)
|
||||
if self.repository.display_version_or_commit == "version":
|
||||
self._update_in_progress(progress=10)
|
||||
self.repository.data.selected_tag = self.latest_version
|
||||
if not version:
|
||||
await self.repository.update_repository(force=True)
|
||||
else:
|
||||
self.repository.ref = version
|
||||
self.repository.data.selected_tag = version
|
||||
self.repository.force_branch = version is not None
|
||||
self._update_in_progress(progress=20)
|
||||
await self.repository.async_install()
|
||||
|
||||
try:
|
||||
await self.repository.async_install(version=version)
|
||||
except HacsException as exception:
|
||||
raise HomeAssistantError(
|
||||
f"Downloading {self.repository.data.full_name} with version {version or self.repository.data.last_version or self.repository.data.last_commit} failed with ({exception})"
|
||||
) from exception
|
||||
finally:
|
||||
self.repository.data.selected_tag = None
|
||||
self.repository.force_branch = False
|
||||
self._update_in_progress(progress=False)
|
||||
|
||||
async def async_release_notes(self) -> str | None:
|
||||
|
||||
@@ -45,9 +45,9 @@ HACS_MANIFEST_JSON_SCHEMA = vol.Schema(
|
||||
vol.Optional("content_in_root"): bool,
|
||||
vol.Optional("country"): _country_validator,
|
||||
vol.Optional("filename"): str,
|
||||
vol.Optional("hacs"): vol.Coerce(AwesomeVersion),
|
||||
vol.Optional("hacs"): str,
|
||||
vol.Optional("hide_default_branch"): bool,
|
||||
vol.Optional("homeassistant"): vol.Coerce(AwesomeVersion),
|
||||
vol.Optional("homeassistant"): str,
|
||||
vol.Optional("persistent_directory"): str,
|
||||
vol.Optional("render_readme"): bool,
|
||||
vol.Optional("zip_release"): bool,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from voluptuous.error import Invalid
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from ..enums import RepositoryFile
|
||||
from ..repositories.base import HacsRepository
|
||||
from ..enums import HacsCategory, RepositoryFile
|
||||
from ..repositories.base import HacsManifest, HacsRepository
|
||||
from ..utils.validate import HACS_MANIFEST_JSON_SCHEMA
|
||||
from .base import ActionValidationBase, ValidationException
|
||||
|
||||
@@ -25,6 +26,10 @@ class Validator(ActionValidationBase):
|
||||
|
||||
content = await self.repository.async_get_hacs_json(self.repository.ref)
|
||||
try:
|
||||
HACS_MANIFEST_JSON_SCHEMA(content)
|
||||
hacsjson = HacsManifest.from_dict(HACS_MANIFEST_JSON_SCHEMA(content))
|
||||
except Invalid as exception:
|
||||
raise ValidationException(exception) from exception
|
||||
raise ValidationException(humanize_error(content, exception)) from exception
|
||||
|
||||
if self.repository.data.category == HacsCategory.INTEGRATION:
|
||||
if hacsjson.zip_release and not hacsjson.filename:
|
||||
raise ValidationException("zip_release is True, but filename is not set")
|
||||
|
||||
Reference in New Issue
Block a user