updating to latest
This commit is contained in:
0
custom_components/hacs/hacsbase/__init__.py
Normal file
0
custom_components/hacs/hacsbase/__init__.py
Normal file
208
custom_components/hacs/hacsbase/data.py
Normal file
208
custom_components/hacs/hacsbase/data.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Data handler for HACS."""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from custom_components.hacs.helpers.classes.manifest import HacsManifest
|
||||
from custom_components.hacs.helpers.functions.register_repository import (
|
||||
register_repository,
|
||||
)
|
||||
from custom_components.hacs.helpers.functions.store import (
|
||||
async_load_from_store,
|
||||
async_save_to_store,
|
||||
async_save_to_store_default_encoder,
|
||||
get_store_for_key,
|
||||
)
|
||||
from custom_components.hacs.share import get_hacs
|
||||
from custom_components.hacs.utils.logger import getLogger
|
||||
|
||||
|
||||
def update_repository_from_storage(repository, storage_data):
|
||||
"""Merge in data from storage into the repo data."""
|
||||
repository.data.memorize_storage(storage_data)
|
||||
repository.data.update_data(storage_data)
|
||||
if repository.data.installed:
|
||||
return
|
||||
|
||||
repository.logger.debug("%s Should be installed but is not... Fixing that!", repository)
|
||||
repository.data.installed = True
|
||||
|
||||
|
||||
class HacsData:
|
||||
"""HacsData class."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize."""
|
||||
self.logger = getLogger()
|
||||
self.hacs = get_hacs()
|
||||
self.content = {}
|
||||
|
||||
async def async_write(self):
|
||||
"""Write content to the store files."""
|
||||
if self.hacs.status.background_task or self.hacs.system.disabled:
|
||||
return
|
||||
|
||||
self.logger.debug("Saving data")
|
||||
|
||||
# Hacs
|
||||
await async_save_to_store(
|
||||
self.hacs.hass,
|
||||
"hacs",
|
||||
{
|
||||
"view": self.hacs.configuration.frontend_mode,
|
||||
"compact": self.hacs.configuration.frontend_compact,
|
||||
"onboarding_done": self.hacs.configuration.onboarding_done,
|
||||
"archived_repositories": self.hacs.common.archived_repositories,
|
||||
"renamed_repositories": self.hacs.common.renamed_repositories,
|
||||
},
|
||||
)
|
||||
await self._async_store_content_and_repos()
|
||||
for event in ("hacs/repository", "hacs/config"):
|
||||
self.hacs.hass.bus.async_fire(event, {})
|
||||
|
||||
async def _async_store_content_and_repos(self): # bb: ignore
|
||||
"""Store the main repos file and each repo that is out of date."""
|
||||
# Repositories
|
||||
self.content = {}
|
||||
# Not run concurrently since this is bound by disk I/O
|
||||
for repository in self.hacs.repositories:
|
||||
await self.async_store_repository_data(repository)
|
||||
|
||||
await async_save_to_store(self.hacs.hass, "repositories", self.content)
|
||||
|
||||
async def async_store_repository_data(self, repository):
|
||||
repository_manifest = repository.repository_manifest.manifest
|
||||
data = {
|
||||
"authors": repository.data.authors,
|
||||
"category": repository.data.category,
|
||||
"description": repository.data.description,
|
||||
"domain": repository.data.domain,
|
||||
"downloads": repository.data.downloads,
|
||||
"etag_repository": repository.data.etag_repository,
|
||||
"full_name": repository.data.full_name,
|
||||
"first_install": repository.status.first_install,
|
||||
"installed_commit": repository.data.installed_commit,
|
||||
"installed": repository.data.installed,
|
||||
"last_commit": repository.data.last_commit,
|
||||
"last_release_tag": repository.data.last_version,
|
||||
"last_updated": repository.data.last_updated,
|
||||
"name": repository.data.name,
|
||||
"new": repository.data.new,
|
||||
"repository_manifest": repository_manifest,
|
||||
"selected_tag": repository.data.selected_tag,
|
||||
"show_beta": repository.data.show_beta,
|
||||
"stars": repository.data.stargazers_count,
|
||||
"topics": repository.data.topics,
|
||||
"version_installed": repository.data.installed_version,
|
||||
}
|
||||
self.content[str(repository.data.id)] = data
|
||||
|
||||
if (
|
||||
repository.data.installed
|
||||
and (repository.data.installed_commit or repository.data.installed_version)
|
||||
and (export := repository.data.export_data())
|
||||
):
|
||||
# export_data will return `None` if the memorized
|
||||
# data is already up to date which allows us to avoid
|
||||
# writing data that is already up to date or generating
|
||||
# executor jobs to check the data on disk to see
|
||||
# if a write is needed.
|
||||
await async_save_to_store_default_encoder(
|
||||
self.hacs.hass,
|
||||
f"hacs/{repository.data.id}.hacs",
|
||||
export,
|
||||
)
|
||||
repository.data.memorize_storage(export)
|
||||
|
||||
async def restore(self):
|
||||
"""Restore saved data."""
|
||||
hacs = await async_load_from_store(self.hacs.hass, "hacs")
|
||||
repositories = await async_load_from_store(self.hacs.hass, "repositories") or {}
|
||||
|
||||
if not hacs and not repositories:
|
||||
# Assume new install
|
||||
self.hacs.status.new = True
|
||||
return True
|
||||
self.logger.info("Restore started")
|
||||
self.hacs.status.new = False
|
||||
|
||||
# Hacs
|
||||
self.hacs.configuration.frontend_mode = hacs.get("view", "Grid")
|
||||
self.hacs.configuration.frontend_compact = hacs.get("compact", False)
|
||||
self.hacs.configuration.onboarding_done = hacs.get("onboarding_done", False)
|
||||
self.hacs.common.archived_repositories = hacs.get("archived_repositories", [])
|
||||
self.hacs.common.renamed_repositories = hacs.get("renamed_repositories", {})
|
||||
|
||||
# Repositories
|
||||
hass = self.hacs.hass
|
||||
stores = {}
|
||||
|
||||
try:
|
||||
await self.register_unknown_repositories(repositories)
|
||||
|
||||
for entry, repo_data in repositories.items():
|
||||
if self.async_restore_repository(entry, repo_data):
|
||||
stores[entry] = get_store_for_key(hass, f"hacs/{entry}.hacs")
|
||||
|
||||
def _load_from_storage():
|
||||
for entry, store in stores.items():
|
||||
if os.path.exists(store.path) and (data := store.load()):
|
||||
update_repository_from_storage(self.hacs.get_by_id(entry), data)
|
||||
|
||||
await hass.async_add_executor_job(_load_from_storage)
|
||||
self.logger.info("Restore done")
|
||||
except (Exception, BaseException) as exception: # pylint: disable=broad-except
|
||||
self.logger.critical(f"[{exception}] Restore Failed!", exc_info=exception)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def register_unknown_repositories(self, repositories):
|
||||
"""Registry any unknown repositories."""
|
||||
register_tasks = [
|
||||
register_repository(repo_data["full_name"], repo_data["category"], False)
|
||||
for entry, repo_data in repositories.items()
|
||||
if not self.hacs.is_known(entry)
|
||||
]
|
||||
if register_tasks:
|
||||
await asyncio.gather(*register_tasks)
|
||||
|
||||
@callback
|
||||
def async_restore_repository(self, entry, repository_data):
|
||||
full_name = repository_data["full_name"]
|
||||
if not (repository := self.hacs.get_by_name(full_name)):
|
||||
self.logger.error(f"Did not find {full_name} ({entry})")
|
||||
return False
|
||||
# Restore repository attributes
|
||||
self.hacs.async_set_repository_id(repository, entry)
|
||||
repository.data.authors = repository_data.get("authors", [])
|
||||
repository.data.description = repository_data.get("description")
|
||||
repository.releases.last_release_object_downloads = repository_data.get("downloads")
|
||||
repository.data.last_updated = repository_data.get("last_updated")
|
||||
repository.data.etag_repository = repository_data.get("etag_repository")
|
||||
repository.data.topics = repository_data.get("topics", [])
|
||||
repository.data.domain = repository_data.get("domain", None)
|
||||
repository.data.stargazers_count = repository_data.get("stars", 0)
|
||||
repository.releases.last_release = repository_data.get("last_release_tag")
|
||||
repository.data.hide = repository_data.get("hide", False)
|
||||
repository.data.installed = repository_data.get("installed", False)
|
||||
repository.data.new = repository_data.get("new", True)
|
||||
repository.data.selected_tag = repository_data.get("selected_tag")
|
||||
repository.data.show_beta = repository_data.get("show_beta", False)
|
||||
repository.data.last_version = repository_data.get("last_release_tag")
|
||||
repository.data.last_commit = repository_data.get("last_commit")
|
||||
repository.data.installed_version = repository_data.get("version_installed")
|
||||
repository.data.installed_commit = repository_data.get("installed_commit")
|
||||
|
||||
repository.repository_manifest = HacsManifest.from_dict(
|
||||
repository_data.get("repository_manifest", {})
|
||||
)
|
||||
|
||||
if repository.data.installed:
|
||||
repository.status.first_install = False
|
||||
|
||||
if repository_data["full_name"] == "hacs/integration":
|
||||
repository.data.installed_version = self.hacs.version
|
||||
repository.data.installed = True
|
||||
|
||||
return True
|
||||
338
custom_components/hacs/hacsbase/hacs.py
Normal file
338
custom_components/hacs/hacsbase/hacs.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""Initialize the HACS base."""
|
||||
from datetime import timedelta
|
||||
|
||||
from aiogithubapi import GitHubException
|
||||
from aiogithubapi.exceptions import GitHubNotModifiedException
|
||||
from queueman import QueueManager
|
||||
from queueman.exceptions import QueueManagerExecutionStillInProgress
|
||||
|
||||
from custom_components.hacs.helpers import HacsHelpers
|
||||
from custom_components.hacs.helpers.functions.get_list_from_default import (
|
||||
async_get_list_from_default,
|
||||
)
|
||||
from custom_components.hacs.helpers.functions.register_repository import (
|
||||
register_repository,
|
||||
)
|
||||
from custom_components.hacs.helpers.functions.store import (
|
||||
async_load_from_store,
|
||||
async_save_to_store,
|
||||
)
|
||||
from custom_components.hacs.share import (
|
||||
get_removed,
|
||||
is_removed,
|
||||
list_removed_repositories,
|
||||
)
|
||||
|
||||
from ..base import HacsBase
|
||||
from ..enums import HacsCategory, HacsStage
|
||||
from ..share import get_factory, get_queue
|
||||
|
||||
|
||||
class Hacs(HacsBase, HacsHelpers):
|
||||
"""The base class of HACS, nested throughout the project."""
|
||||
|
||||
factory = get_factory()
|
||||
queue = get_queue()
|
||||
|
||||
@property
|
||||
def repositories(self):
|
||||
"""Return the full repositories list."""
|
||||
return self._repositories
|
||||
|
||||
def async_set_repositories(self, repositories):
|
||||
"""Set the list of repositories."""
|
||||
self._repositories = []
|
||||
self._repositories_by_id = {}
|
||||
self._repositories_by_full_name = {}
|
||||
|
||||
for repository in repositories:
|
||||
self.async_add_repository(repository)
|
||||
|
||||
def async_set_repository_id(self, repository, repo_id):
|
||||
"""Update a repository id."""
|
||||
existing_repo_id = str(repository.data.id)
|
||||
if existing_repo_id == repo_id:
|
||||
return
|
||||
if existing_repo_id != "0":
|
||||
raise ValueError(
|
||||
f"The repo id for {repository.data.full_name_lower} is already set to {existing_repo_id}"
|
||||
)
|
||||
repository.data.id = repo_id
|
||||
self._repositories_by_id[repo_id] = repository
|
||||
|
||||
def async_add_repository(self, repository):
|
||||
"""Add a repository to the list."""
|
||||
if repository.data.full_name_lower in self._repositories_by_full_name:
|
||||
raise ValueError(f"The repo {repository.data.full_name_lower} is already added")
|
||||
self._repositories.append(repository)
|
||||
repo_id = str(repository.data.id)
|
||||
if repo_id != "0":
|
||||
self._repositories_by_id[repo_id] = repository
|
||||
self._repositories_by_full_name[repository.data.full_name_lower] = repository
|
||||
|
||||
def async_remove_repository(self, repository):
|
||||
"""Remove a repository from the list."""
|
||||
if repository.data.full_name_lower not in self._repositories_by_full_name:
|
||||
return
|
||||
self._repositories.remove(repository)
|
||||
repo_id = str(repository.data.id)
|
||||
if repo_id in self._repositories_by_id:
|
||||
del self._repositories_by_id[repo_id]
|
||||
del self._repositories_by_full_name[repository.data.full_name_lower]
|
||||
|
||||
def get_by_id(self, repository_id):
|
||||
"""Get repository by ID."""
|
||||
return self._repositories_by_id.get(str(repository_id))
|
||||
|
||||
def get_by_name(self, repository_full_name):
|
||||
"""Get repository by full_name."""
|
||||
if repository_full_name is None:
|
||||
return None
|
||||
return self._repositories_by_full_name.get(repository_full_name.lower())
|
||||
|
||||
def is_known(self, repository_id):
|
||||
"""Return a bool if the repository is known."""
|
||||
return str(repository_id) in self._repositories_by_id
|
||||
|
||||
@property
|
||||
def sorted_by_name(self):
|
||||
"""Return a sorted(by name) list of repository objects."""
|
||||
return sorted(self.repositories, key=lambda x: x.display_name)
|
||||
|
||||
@property
|
||||
def sorted_by_repository_name(self):
|
||||
"""Return a sorted(by repository_name) list of repository objects."""
|
||||
return sorted(self.repositories, key=lambda x: x.data.full_name)
|
||||
|
||||
async def register_repository(self, full_name, category, check=True):
|
||||
"""Register a repository."""
|
||||
await register_repository(full_name, category, check=check)
|
||||
|
||||
async def startup_tasks(self, _event=None):
|
||||
"""Tasks that are started after startup."""
|
||||
await self.async_set_stage(HacsStage.STARTUP)
|
||||
self.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
await self.handle_critical_repositories_startup()
|
||||
await self.async_load_default_repositories()
|
||||
await self.clear_out_removed_repositories()
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.recurring_tasks_installed, timedelta(hours=2)
|
||||
)
|
||||
)
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.recurring_tasks_all, timedelta(hours=25)
|
||||
)
|
||||
)
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.prosess_queue, timedelta(minutes=10)
|
||||
)
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire("hacs/reload", {"force": True})
|
||||
await self.recurring_tasks_installed()
|
||||
|
||||
await self.prosess_queue()
|
||||
|
||||
self.status.startup = False
|
||||
self.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.async_set_stage(HacsStage.RUNNING)
|
||||
|
||||
async def handle_critical_repositories_startup(self):
|
||||
"""Handled critical repositories during startup."""
|
||||
alert = False
|
||||
critical = await async_load_from_store(self.hass, "critical")
|
||||
if not critical:
|
||||
return
|
||||
for repo in critical:
|
||||
if not repo["acknowledged"]:
|
||||
alert = True
|
||||
if alert:
|
||||
self.log.critical("URGENT!: Check the HACS panel!")
|
||||
self.hass.components.persistent_notification.create(
|
||||
title="URGENT!", message="**Check the HACS panel!**"
|
||||
)
|
||||
|
||||
async def handle_critical_repositories(self):
|
||||
"""Handled critical repositories during runtime."""
|
||||
# Get critical repositories
|
||||
critical_queue = QueueManager()
|
||||
instored = []
|
||||
critical = []
|
||||
was_installed = False
|
||||
|
||||
try:
|
||||
critical = await self.async_github_get_hacs_default_file("critical")
|
||||
except GitHubNotModifiedException:
|
||||
return
|
||||
except GitHubException:
|
||||
pass
|
||||
|
||||
if not critical:
|
||||
self.log.debug("No critical repositories")
|
||||
return
|
||||
|
||||
stored_critical = await async_load_from_store(self.hass, "critical")
|
||||
|
||||
for stored in stored_critical or []:
|
||||
instored.append(stored["repository"])
|
||||
|
||||
stored_critical = []
|
||||
|
||||
for repository in critical:
|
||||
removed_repo = get_removed(repository["repository"])
|
||||
removed_repo.removal_type = "critical"
|
||||
repo = self.get_by_name(repository["repository"])
|
||||
|
||||
stored = {
|
||||
"repository": repository["repository"],
|
||||
"reason": repository["reason"],
|
||||
"link": repository["link"],
|
||||
"acknowledged": True,
|
||||
}
|
||||
if repository["repository"] not in instored:
|
||||
if repo is not None and repo.installed:
|
||||
self.log.critical(
|
||||
"Removing repository %s, it is marked as critical",
|
||||
repository["repository"],
|
||||
)
|
||||
was_installed = True
|
||||
stored["acknowledged"] = False
|
||||
# Remove from HACS
|
||||
critical_queue.add(repository.uninstall())
|
||||
repo.remove()
|
||||
|
||||
stored_critical.append(stored)
|
||||
removed_repo.update_data(stored)
|
||||
|
||||
# Uninstall
|
||||
await critical_queue.execute()
|
||||
|
||||
# Save to FS
|
||||
await async_save_to_store(self.hass, "critical", stored_critical)
|
||||
|
||||
# Restart HASS
|
||||
if was_installed:
|
||||
self.log.critical("Resarting Home Assistant")
|
||||
self.hass.async_create_task(self.hass.async_stop(100))
|
||||
|
||||
async def prosess_queue(self, _notarealarg=None):
|
||||
"""Recurring tasks for installed repositories."""
|
||||
if not self.queue.has_pending_tasks:
|
||||
self.log.debug("Nothing in the queue")
|
||||
return
|
||||
if self.queue.running:
|
||||
self.log.debug("Queue is already running")
|
||||
return
|
||||
|
||||
can_update = await self.async_can_update()
|
||||
self.log.debug(
|
||||
"Can update %s repositories, items in queue %s",
|
||||
can_update,
|
||||
self.queue.pending_tasks,
|
||||
)
|
||||
if can_update != 0:
|
||||
self.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
try:
|
||||
await self.queue.execute(can_update)
|
||||
except QueueManagerExecutionStillInProgress:
|
||||
pass
|
||||
self.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
async def recurring_tasks_installed(self, _notarealarg=None):
|
||||
"""Recurring tasks for installed repositories."""
|
||||
self.log.debug("Starting recurring background task for installed repositories")
|
||||
self.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
for repository in self.repositories:
|
||||
if self.status.startup and repository.data.full_name == "hacs/integration":
|
||||
continue
|
||||
if repository.data.installed and repository.data.category in self.common.categories:
|
||||
self.queue.add(self.factory.safe_update(repository))
|
||||
|
||||
await self.handle_critical_repositories()
|
||||
self.status.background_task = False
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
await self.data.async_write()
|
||||
self.log.debug("Recurring background task for installed repositories done")
|
||||
|
||||
async def recurring_tasks_all(self, _notarealarg=None):
|
||||
"""Recurring tasks for all repositories."""
|
||||
self.log.debug("Starting recurring background task for all repositories")
|
||||
self.status.background_task = True
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
|
||||
for repository in self.repositories:
|
||||
if repository.data.category in self.common.categories:
|
||||
self.queue.add(self.factory.safe_common_update(repository))
|
||||
|
||||
await self.async_load_default_repositories()
|
||||
await self.clear_out_removed_repositories()
|
||||
self.status.background_task = False
|
||||
await self.data.async_write()
|
||||
self.hass.bus.async_fire("hacs/status", {})
|
||||
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
|
||||
self.log.debug("Recurring background task for all repositories done")
|
||||
|
||||
async def clear_out_removed_repositories(self):
|
||||
"""Clear out blaclisted repositories."""
|
||||
need_to_save = False
|
||||
for removed in list_removed_repositories():
|
||||
repository = self.get_by_name(removed.repository)
|
||||
if repository is not None:
|
||||
if repository.data.installed and removed.removal_type != "critical":
|
||||
self.log.warning(
|
||||
f"You have {repository.data.full_name} installed with HACS "
|
||||
+ "this repository has been removed, please consider removing it. "
|
||||
+ f"Removal reason ({removed.removal_type})"
|
||||
)
|
||||
else:
|
||||
need_to_save = True
|
||||
repository.remove()
|
||||
|
||||
if need_to_save:
|
||||
await self.data.async_write()
|
||||
|
||||
async def async_load_default_repositories(self):
|
||||
"""Load known repositories."""
|
||||
self.log.info("Loading known repositories")
|
||||
|
||||
for item in await async_get_list_from_default(HacsCategory.REMOVED):
|
||||
removed = get_removed(item["repository"])
|
||||
removed.reason = item.get("reason")
|
||||
removed.link = item.get("link")
|
||||
removed.removal_type = item.get("removal_type")
|
||||
|
||||
for category in self.common.categories or []:
|
||||
self.queue.add(self.async_get_category_repositories(HacsCategory(category)))
|
||||
|
||||
await self.prosess_queue()
|
||||
|
||||
async def async_get_category_repositories(self, category: HacsCategory):
|
||||
"""Get repositories from category."""
|
||||
repositories = await async_get_list_from_default(category)
|
||||
for repo in repositories:
|
||||
if self.common.renamed_repositories.get(repo):
|
||||
repo = self.common.renamed_repositories[repo]
|
||||
if is_removed(repo):
|
||||
continue
|
||||
if repo in self.common.archived_repositories:
|
||||
continue
|
||||
repository = self.get_by_name(repo)
|
||||
if repository is not None:
|
||||
if str(repository.data.id) not in self.common.default:
|
||||
self.common.default.append(str(repository.data.id))
|
||||
else:
|
||||
continue
|
||||
continue
|
||||
self.queue.add(self.factory.safe_register(repo, category))
|
||||
Reference in New Issue
Block a user