Add HACS, Themes

This commit is contained in:
2022-05-04 10:50:54 -07:00
parent af527f1e65
commit 9c7c4a5863
183 changed files with 16569 additions and 17 deletions

View File

@@ -0,0 +1 @@
"""Init HACS tasks."""

View File

@@ -0,0 +1,36 @@
"""Starting setup task: extra stores."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsCategory, HacsStage
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Set up extra stores in HACS if enabled in Home Assistant."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
self.hacs.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
self.hacs.enable_hacs_category(HacsCategory(category))
if HacsCategory.PYTHON_SCRIPT in self.hacs.hass.config.components:
self.hacs.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
if self.hacs.hass.services.has_service("frontend", "reload_themes"):
self.hacs.enable_hacs_category(HacsCategory.THEME)
if self.hacs.configuration.appdaemon:
self.hacs.enable_hacs_category(HacsCategory.APPDAEMON)
if self.hacs.configuration.netdaemon:
self.hacs.enable_hacs_category(HacsCategory.NETDAEMON)

View File

@@ -0,0 +1,59 @@
""""Hacs base setup task."""
# pylint: disable=abstract-method
from __future__ import annotations
from datetime import timedelta
from logging import Handler
from time import monotonic
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsStage
class HacsTask:
"""Hacs task base."""
events: list[str] | None = None
schedule: timedelta | None = None
stages: list[HacsStage] | None = None
_can_run_disabled = False ## Set to True if task can run while disabled
def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None:
self.hacs = hacs
self.hass = hass
@property
def slug(self) -> str:
"""Return the check slug."""
return self.__class__.__module__.rsplit(".", maxsplit=1)[-1]
def task_logger(self, handler: Handler, msg: str) -> None:
"""Log message from task"""
handler("<HacsTask %s> %s", self.slug, msg)
async def execute_task(self, *_, **__) -> None:
"""Execute the task defined in subclass."""
if not self._can_run_disabled and self.hacs.system.disabled:
self.task_logger(
self.hacs.log.debug,
f"Skipping task, HACS is disabled {self.hacs.system.disabled_reason}",
)
return
self.task_logger(self.hacs.log.debug, "Executing task")
start_time = monotonic()
try:
if task := getattr(self, "async_execute", None):
await task() # pylint: disable=not-callable
elif task := getattr(self, "execute", None):
await self.hass.async_add_executor_job(task)
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
self.task_logger(self.hacs.log.error, f"failed: {exception}")
else:
self.hacs.log.debug(
"<HacsTask %s> took %.3f seconds to complete", self.slug, monotonic() - start_time
)

View File

@@ -0,0 +1,48 @@
""""Starting setup task: Constrains"."""
from __future__ import annotations
import os
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..const import MINIMUM_HA_VERSION
from ..enums import HacsDisabledReason, HacsStage
from ..utils.version import version_left_higher_or_equal_then_right
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Check env Constrains."""
stages = [HacsStage.SETUP]
def execute(self) -> None:
"""Execute the task."""
for location in (
self.hass.config.path("custom_components/custom_updater.py"),
self.hass.config.path("custom_components/custom_updater/__init__.py"),
):
if os.path.exists(location):
self.task_logger(
self.hacs.log.critical,
"This cannot be used with custom_updater. "
f"To use this you need to remove custom_updater form {location}",
)
self.hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
if not version_left_higher_or_equal_then_right(
self.hacs.core.ha_version.string,
MINIMUM_HA_VERSION,
):
self.task_logger(
self.hacs.log.critical,
f"You need HA version {MINIMUM_HA_VERSION} or newer to use this integration.",
)
self.hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)

View File

@@ -0,0 +1,36 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsDisabledReason
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
""" "Hacs task base."""
_can_run_disabled = True
schedule = timedelta(minutes=5)
async def async_execute(self) -> None:
"""Execute the task."""
if (
not self.hacs.system.disabled
or self.hacs.system.disabled_reason != HacsDisabledReason.RATE_LIMIT
):
return
self.task_logger(self.hacs.log.debug, "Checking if ratelimit has lifted")
can_update = await self.hacs.async_can_update()
self.task_logger(self.hacs.log.debug, f"Ratelimit indicate we can update {can_update}")
if can_update > 0:
self.hacs.enable_hacs()

View File

@@ -0,0 +1,29 @@
"""Starting setup task: clear storage."""
from __future__ import annotations
import os
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsStage
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Clear old files from storage."""
stages = [HacsStage.SETUP]
def execute(self) -> None:
"""Execute the task."""
for storage_file in ("hacs",):
path = f"{self.hacs.core.config_path}/.storage/{storage_file}"
if os.path.isfile(path):
self.task_logger(self.hacs.log.info, f"Cleaning up old storage file: {path}")
os.remove(path)

View File

@@ -0,0 +1,35 @@
""""Hacs base setup task."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsStage
from ..utils.store import async_load_from_store
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs notify critical during startup task."""
stages = [HacsStage.STARTUP]
async def async_execute(self) -> None:
"""Execute the task."""
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.hacs.log.critical("URGENT!: Check the HACS panel!")
self.hass.components.persistent_notification.create(
title="URGENT!", message="**Check the HACS panel!**"
)

View File

@@ -0,0 +1,51 @@
"""Starting setup task: load HACS repository."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsCategory, HacsDisabledReason, HacsGitHubRepo, HacsStage
from ..exceptions import HacsException
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Load HACS repositroy."""
stages = [HacsStage.STARTUP]
async def async_execute(self) -> None:
"""Execute the task."""
try:
repository = self.hacs.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
if repository is None:
await self.hacs.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION,
default=True,
)
repository = self.hacs.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
if repository is None:
raise HacsException("Unknown error")
repository.data.installed = True
repository.data.installed_version = self.hacs.integration.version
repository.data.new = False
repository.data.releases = True
self.hacs.repository = repository.repository_object
self.hacs.repositories.mark_default(repository)
except HacsException as exception:
if "403" in f"{exception}":
self.task_logger(
self.hacs.log.critical,
"GitHub API is ratelimited, or the token is wrong.",
)
else:
self.task_logger(self.hacs.log.critical, f"[{exception}] - Could not load HACS!")
self.hacs.disable_hacs(HacsDisabledReason.LOAD_HACS)

View File

@@ -0,0 +1,74 @@
"""Hacs task manager."""
from __future__ import annotations
import asyncio
from importlib import import_module
from pathlib import Path
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from .base import HacsTask
class HacsTaskManager:
"""Hacs task manager."""
def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None:
"""Initialize the setup manager class."""
self.hacs = hacs
self.hass = hass
self.__tasks: dict[str, HacsTask] = {}
@property
def tasks(self) -> list[HacsTask]:
"""Return all list of all tasks."""
return list(self.__tasks.values())
async def async_load(self) -> None:
"""Load all tasks."""
task_files = Path(__file__).parent
task_modules = (
module.stem
for module in task_files.glob("*.py")
if module.name not in ("base.py", "__init__.py", "manager.py")
)
async def _load_module(module: str):
task_module = import_module(f"{__package__}.{module}")
if task := await task_module.async_setup_task(hacs=self.hacs, hass=self.hass):
self.__tasks[task.slug] = task
await asyncio.gather(*[_load_module(task) for task in task_modules])
self.hacs.log.info("Loaded %s tasks", len(self.tasks))
schedule_tasks = len(self.hacs.recuring_tasks) == 0
for task in self.tasks:
if task.events is not None:
for event in task.events:
self.hass.bus.async_listen_once(event, task.execute_task)
if task.schedule is not None and schedule_tasks:
self.hacs.log.debug(
"Scheduling <HacsTask %s> to run every %s", task.slug, task.schedule
)
self.hacs.recuring_tasks.append(
self.hacs.hass.helpers.event.async_track_time_interval(
task.execute_task, task.schedule
)
)
def get(self, slug: str) -> HacsTask | None:
"""Return a task."""
return self.__tasks.get(slug)
async def async_execute_runtume_tasks(self) -> None:
"""Execute the the execute methods of each runtime task if the stage matches."""
await asyncio.gather(
*(
task.execute_task()
for task in self.tasks
if task.stages is not None and self.hacs.stage in task.stages
)
)

View File

@@ -0,0 +1,49 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..exceptions import HacsExecutionStillInProgress
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
""" "Hacs task base."""
schedule = timedelta(minutes=10)
async def async_execute(self) -> None:
"""Execute the task."""
if not self.hacs.queue.has_pending_tasks:
self.task_logger(self.hacs.log.debug, "Nothing in the queue")
return
if self.hacs.queue.running:
self.task_logger(self.hacs.log.debug, "Queue is already running")
return
async def _handle_queue():
if not self.hacs.queue.has_pending_tasks:
return
can_update = await self.hacs.async_can_update()
self.task_logger(
self.hacs.log.debug,
f"Can update {can_update} repositories, "
f"items in queue {self.hacs.queue.pending_tasks}",
)
if can_update != 0:
try:
await self.hacs.queue.execute(can_update)
except HacsExecutionStillInProgress:
return
await _handle_queue()
await _handle_queue()

View File

@@ -0,0 +1,24 @@
""""Starting setup task: Restore"."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsDisabledReason, HacsStage
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Restore HACS data."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
if not await self.hacs.data.restore():
self.hacs.disable_hacs(HacsDisabledReason.RESTORE)

View File

@@ -0,0 +1,106 @@
""""Starting setup task: Frontend"."""
from __future__ import annotations
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..const import DOMAIN
from ..enums import HacsStage
from ..hacs_frontend import locate_dir
from ..hacs_frontend.version import VERSION as FE_VERSION
from .base import HacsTask
URL_BASE = "/hacsfiles"
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Setup the HACS frontend."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
# Register themes
self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes"))
# Register frontend
if self.hacs.configuration.frontend_repo_url:
self.task_logger(
self.hacs.log.warning,
"Frontend development mode enabled. Do not run in production!",
)
self.hass.http.register_view(HacsFrontendDev())
else:
#
self.hass.http.register_static_path(
f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
)
# Custom iconset
self.hass.http.register_static_path(
f"{URL_BASE}/iconset.js", str(self.hacs.integration_dir / "iconset.js")
)
if "frontend_extra_module_url" not in self.hass.data:
self.hass.data["frontend_extra_module_url"] = set()
self.hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
# Register www/community for all other files
use_cache = self.hacs.core.lovelace_mode == "storage"
self.task_logger(
self.hacs.log.info,
f"{self.hacs.core.lovelace_mode} mode, cache for /hacsfiles/: {use_cache}",
)
self.hass.http.register_static_path(
URL_BASE,
self.hass.config.path("www/community"),
cache_headers=use_cache,
)
self.hacs.frontend_version = FE_VERSION
# Add to sidepanel if needed
if DOMAIN not in self.hass.data.get("frontend_panels", {}):
self.hass.components.frontend.async_register_built_in_panel(
component_name="custom",
sidebar_title=self.hacs.configuration.sidepanel_title,
sidebar_icon=self.hacs.configuration.sidepanel_icon,
frontend_url_path=DOMAIN,
config={
"_panel_custom": {
"name": "hacs-frontend",
"embed_iframe": True,
"trust_external": False,
"js_url": f"/hacsfiles/frontend/entrypoint.js?hacstag={FE_VERSION}",
}
},
require_admin=True,
)
class HacsFrontendDev(HomeAssistantView):
"""Dev View Class for HACS."""
requires_auth = False
name = "hacs_files:frontend"
url = r"/hacsfiles/frontend/{requested_file:.+}"
async def get(self, request, requested_file): # pylint: disable=unused-argument
"""Handle HACS Web requests."""
hacs: HacsBase = request.app["hass"].data.get(DOMAIN)
requested = requested_file.split("/")[-1]
request = await hacs.session.get(f"{hacs.configuration.frontend_repo_url}/{requested}")
if request.status == 200:
result = await request.read()
response = web.Response(body=result)
response.headers["Content-Type"] = "application/javascript"
return response

View File

@@ -0,0 +1,36 @@
""""Starting setup task: Sensor"."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers.discovery import async_load_platform
from ..base import HacsBase
from ..const import DOMAIN
from ..enums import ConfigurationType, HacsStage
from .base import HacsTask
SENSOR_DOMAIN = "sensor"
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Setup the HACS sensor platform."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
if self.hacs.configuration.config_type == ConfigurationType.YAML:
self.hass.async_create_task(
async_load_platform(
self.hass, SENSOR_DOMAIN, DOMAIN, {}, self.hacs.configuration.config
)
)
else:
self.hass.config_entries.async_setup_platforms(
self.hacs.configuration.config_entry, [SENSOR_DOMAIN]
)

View File

@@ -0,0 +1,32 @@
""""Starting setup task: Update"."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import ConfigurationType, HacsStage
from .base import HacsTask
UPDATE_DOMAIN = "update"
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Setup the HACS update platform."""
stages = [HacsStage.RUNNING]
async def async_execute(self) -> None:
"""Execute the task."""
if self.hacs.configuration.config_type == ConfigurationType.YAML:
self.task_logger(
self.hacs.log.info, "Update entities are only supported when using UI configuration"
)
elif self.hacs.core.ha_version >= "2022.4.0.dev0" and self.hacs.configuration.experimental:
self.hass.config_entries.async_setup_platforms(
self.hacs.configuration.config_entry, [UPDATE_DOMAIN]
)

View File

@@ -0,0 +1,490 @@
"""Register WS API endpoints for HACS."""
from __future__ import annotations
import sys
from aiogithubapi import AIOGitHubAPIException
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import async_register_command
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.hacs.const import DOMAIN
from ..base import HacsBase
from ..enums import HacsStage
from ..exceptions import HacsException
from ..utils import regex
from ..utils.store import async_load_from_store, async_save_to_store
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Setup the HACS websocket API."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
async_register_command(self.hass, hacs_settings)
async_register_command(self.hass, hacs_config)
async_register_command(self.hass, hacs_repositories)
async_register_command(self.hass, hacs_repository)
async_register_command(self.hass, hacs_repository_data)
async_register_command(self.hass, hacs_status)
async_register_command(self.hass, hacs_removed)
async_register_command(self.hass, acknowledge_critical_repository)
async_register_command(self.hass, get_critical_repositories)
async_register_command(self.hass, hacs_repository_ignore)
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/critical",
vol.Optional("repository"): cv.string,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def acknowledge_critical_repository(hass, connection, msg):
"""Handle get media player cover command."""
repository = msg["repository"]
critical = await async_load_from_store(hass, "critical")
for repo in critical:
if repository == repo["repository"]:
repo["acknowledged"] = True
await async_save_to_store(hass, "critical", critical)
connection.send_message(websocket_api.result_message(msg["id"], critical))
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/get_critical",
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def get_critical_repositories(hass, connection, msg):
"""Handle get media player cover command."""
critical = await async_load_from_store(hass, "critical")
if not critical:
critical = []
connection.send_message(websocket_api.result_message(msg["id"], critical))
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/config",
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_config(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
connection.send_message(
websocket_api.result_message(
msg["id"],
{
"frontend_mode": hacs.configuration.frontend_mode,
"frontend_compact": hacs.configuration.frontend_compact,
"onboarding_done": hacs.configuration.onboarding_done,
"version": hacs.version,
"frontend_expected": hacs.frontend_version,
"frontend_running": hacs.frontend_version,
"dev": hacs.configuration.dev,
"debug": hacs.configuration.debug,
"country": hacs.configuration.country,
"experimental": hacs.configuration.experimental,
"categories": hacs.common.categories,
},
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/removed",
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_removed(hass, connection, msg):
"""Get information about removed repositories."""
hacs: HacsBase = hass.data.get(DOMAIN)
content = []
for repo in hacs.repositories.list_removed:
if repo.repository not in hacs.common.ignored_repositories:
content.append(repo.to_json())
connection.send_message(websocket_api.result_message(msg["id"], content))
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repositories",
vol.Optional("categories"): [str],
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_repositories(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
connection.send_message(
websocket_api.result_message(
msg["id"],
[
{
"additional_info": repo.additional_info,
"authors": repo.data.authors,
"available_version": repo.display_available_version,
"beta": repo.data.show_beta,
"can_install": repo.can_download,
"category": repo.data.category,
"config_flow": repo.data.config_flow,
"country": repo.data.country,
"custom": not hacs.repositories.is_default(str(repo.data.id)),
"default_branch": repo.data.default_branch,
"description": repo.data.description,
"domain": repo.data.domain,
"downloads": repo.data.downloads,
"file_name": repo.data.file_name,
"first_install": repo.status.first_install,
"full_name": repo.data.full_name,
"hide_default_branch": repo.data.hide_default_branch,
"hide": repo.data.hide,
"homeassistant": repo.data.homeassistant,
"id": repo.data.id,
"info": None,
"installed_version": repo.display_installed_version,
"installed": repo.data.installed,
"issues": repo.data.open_issues,
"javascript_type": None,
"last_updated": repo.data.last_updated,
"local_path": repo.content.path.local,
"main_action": repo.main_action,
"name": repo.display_name,
"new": repo.data.new,
"pending_upgrade": repo.pending_update,
"releases": repo.data.published_tags,
"selected_tag": repo.data.selected_tag,
"stars": repo.data.stargazers_count,
"state": repo.state,
"status_description": repo.display_status_description,
"status": repo.display_status,
"topics": repo.data.topics,
"updated_info": repo.status.updated_info,
"version_or_commit": repo.display_version_or_commit,
}
for repo in hacs.repositories.list_all
if repo.data.category in (msg.get("categories") or hacs.common.categories)
and not repo.ignored_by_country_configuration
],
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository/data",
vol.Optional("action"): cv.string,
vol.Optional("repository"): cv.string,
vol.Optional("data"): cv.string,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_repository_data(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
repo_id = msg.get("repository")
action = msg.get("action")
data = msg.get("data")
if repo_id is None:
return
if action == "add":
repo_id = regex.extract_repository_from_url(repo_id)
if repo_id is None:
return
if repo_id in hacs.common.skip:
hacs.common.skip.remove(repo_id)
if hacs.common.renamed_repositories.get(repo_id):
repo_id = hacs.common.renamed_repositories[repo_id]
if not hacs.repositories.get_by_full_name(repo_id):
try:
registration = await hacs.async_register_repository(
repository_full_name=repo_id, category=data.lower()
)
if registration is not None:
raise HacsException(registration)
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
hass.bus.async_fire(
"hacs/error",
{
"action": "add_repository",
"exception": str(sys.exc_info()[0].__name__),
"message": str(exception),
},
)
else:
hass.bus.async_fire(
"hacs/error",
{
"action": "add_repository",
"message": f"Repository '{repo_id}' exists in the store.",
},
)
repository = hacs.repositories.get_by_full_name(repo_id)
else:
repository = hacs.repositories.get_by_id(repo_id)
if repository is None:
hass.bus.async_fire("hacs/repository", {})
return
hacs.log.debug("Running %s for %s", action, repository.data.full_name)
try:
if action == "set_state":
repository.state = data
elif action == "set_version":
repository.data.selected_tag = data
await repository.update_repository(force=True)
repository.state = None
elif action == "install":
was_installed = repository.data.installed
repository.data.selected_tag = data
await repository.update_repository(force=True)
await repository.async_install()
repository.state = None
if not was_installed:
hass.bus.async_fire("hacs/reload", {"force": True})
await hacs.async_recreate_entities()
elif action == "add":
repository.state = None
else:
repository.state = None
hacs.log.error("WS action '%s' is not valid", action)
message = None
except AIOGitHubAPIException as exception:
message = exception
except AttributeError as exception:
message = f"Could not use repository with ID {repo_id} ({exception})"
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
message = exception
if message is not None:
hacs.log.error(message)
hass.bus.async_fire("hacs/error", {"message": str(message)})
await hacs.data.async_write()
connection.send_message(websocket_api.result_message(msg["id"], {}))
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository",
vol.Optional("action"): cv.string,
vol.Optional("repository"): cv.string,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_repository(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
data = {}
repository = None
repo_id = msg.get("repository")
action = msg.get("action")
if repo_id is None or action is None:
return
try:
repository = hacs.repositories.get_by_id(repo_id)
hacs.log.debug(f"Running {action} for {repository.data.full_name}")
if action == "update":
await repository.update_repository(ignore_issues=True, force=True)
repository.status.updated_info = True
elif action == "install":
repository.data.new = False
was_installed = repository.data.installed
await repository.async_install()
if not was_installed:
hass.bus.async_fire("hacs/reload", {"force": True})
await hacs.async_recreate_entities()
elif action == "not_new":
repository.data.new = False
elif action == "uninstall":
repository.data.new = False
await repository.update_repository(ignore_issues=True, force=True)
await repository.uninstall()
elif action == "hide":
repository.data.hide = True
elif action == "unhide":
repository.data.hide = False
elif action == "show_beta":
repository.data.show_beta = True
await repository.update_repository(force=True)
elif action == "hide_beta":
repository.data.show_beta = False
await repository.update_repository(force=True)
elif action == "toggle_beta":
repository.data.show_beta = not repository.data.show_beta
await repository.update_repository(force=True)
elif action == "delete":
repository.data.show_beta = False
repository.remove()
elif action == "release_notes":
data = [
{
"name": x.name,
"body": x.body,
"tag": x.tag_name,
}
for x in repository.releases.objects
]
elif action == "set_version":
if msg["version"] == repository.data.default_branch:
repository.data.selected_tag = None
else:
repository.data.selected_tag = msg["version"]
await repository.update_repository(force=True)
hass.bus.async_fire("hacs/reload", {"force": True})
else:
hacs.log.error(f"WS action '{action}' is not valid")
await hacs.data.async_write()
message = None
except AIOGitHubAPIException as exception:
message = exception
except AttributeError as exception:
message = f"Could not use repository with ID {repo_id} ({exception})"
except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
message = exception
if message is not None:
hacs.log.error(message)
hass.bus.async_fire("hacs/error", {"message": str(message)})
if repository:
repository.state = None
connection.send_message(websocket_api.result_message(msg["id"], data))
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/settings",
vol.Optional("action"): cv.string,
vol.Optional("categories"): cv.ensure_list,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_settings(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
action = msg["action"]
hacs.log.debug("WS action '%s'", action)
if action == "set_fe_grid":
hacs.configuration.frontend_mode = "Grid"
elif action == "onboarding_done":
hacs.configuration.onboarding_done = True
elif action == "set_fe_table":
hacs.configuration.frontend_mode = "Table"
elif action == "set_fe_compact_true":
hacs.configuration.frontend_compact = False
elif action == "set_fe_compact_false":
hacs.configuration.frontend_compact = True
elif action == "clear_new":
for repo in hacs.repositories.list_all:
if repo.data.new and repo.data.category in msg.get("categories", []):
hacs.log.debug(
"Clearing new flag from '%s'",
repo.data.full_name,
)
repo.data.new = False
else:
hacs.log.error("WS action '%s' is not valid", action)
hass.bus.async_fire("hacs/config", {})
await hacs.data.async_write()
connection.send_message(websocket_api.result_message(msg["id"], {}))
@websocket_api.websocket_command({vol.Required("type"): "hacs/status"})
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_status(hass, connection, msg):
"""Handle get media player cover command."""
hacs: HacsBase = hass.data.get(DOMAIN)
connection.send_message(
websocket_api.result_message(
msg["id"],
{
"startup": hacs.status.startup,
"background_task": False,
"lovelace_mode": hacs.core.lovelace_mode,
"reloading_data": hacs.status.reloading_data,
"upgrading_all": hacs.status.upgrading_all,
"disabled": hacs.system.disabled,
"disabled_reason": hacs.system.disabled_reason,
"has_pending_tasks": hacs.queue.has_pending_tasks,
"stage": hacs.stage,
},
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "hacs/repository/ignore",
vol.Required("repository"): str,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def hacs_repository_ignore(hass, connection, msg):
"""Ignore a repository."""
hacs: HacsBase = hass.data.get(DOMAIN)
hacs.common.ignored_repositories.append(msg["repository"])
connection.send_message(websocket_api.result_message(msg["id"]))

View File

@@ -0,0 +1,24 @@
""""Store HACS data."""
from __future__ import annotations
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
""" "Hacs task base."""
events = [EVENT_HOMEASSISTANT_FINAL_WRITE]
_can_run_disabled = True
async def async_execute(self) -> None:
"""Execute the task."""
await self.hacs.data.async_write(force=True)

View File

@@ -0,0 +1,34 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs update all task."""
schedule = timedelta(hours=25)
async def async_execute(self) -> None:
"""Execute the task."""
self.task_logger(
self.hacs.log.debug, "Starting recurring background task for all repositories"
)
for repository in self.hacs.repositories.list_all:
if repository.data.category in self.hacs.common.categories:
self.hacs.queue.add(repository.common_update())
await self.hacs.data.async_write()
self.hass.bus.async_fire("hacs/repository", {"action": "reload"})
self.task_logger(self.hacs.log.debug, "Recurring background task for all repositories done")

View File

@@ -0,0 +1,92 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from aiogithubapi import GitHubNotModifiedException
from homeassistant.core import HomeAssistant
from custom_components.hacs.utils.queue_manager import QueueManager
from custom_components.hacs.utils.store import (
async_load_from_store,
async_save_to_store,
)
from ..base import HacsBase
from ..enums import HacsStage
from ..exceptions import HacsException
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs update critical task."""
schedule = timedelta(hours=2)
stages = [HacsStage.RUNNING]
async def async_execute(self) -> None:
"""Execute the task."""
critical_queue = QueueManager(hass=self.hass)
instored = []
critical = []
was_installed = False
try:
critical = await self.hacs.async_github_get_hacs_default_file("critical")
except GitHubNotModifiedException:
return
except HacsException:
pass
if not critical:
self.hacs.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 = self.hacs.repositories.removed_repository(repository["repository"])
removed_repo.removal_type = "critical"
repo = self.hacs.repositories.get_by_full_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.data.installed:
self.hacs.log.critical(
"Removing repository %s, it is marked as critical",
repository["repository"],
)
was_installed = True
stored["acknowledged"] = False
# Remove from HACS
critical_queue.add(repo.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.hacs.log.critical("Resarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))

View File

@@ -0,0 +1,64 @@
""""Hacs base setup task."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsCategory, HacsStage
from ..exceptions import HacsException
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs update default task."""
schedule = timedelta(hours=3)
stages = [HacsStage.STARTUP]
async def async_execute(self) -> None:
"""Execute the task."""
self.hacs.log.info("Loading known repositories")
await asyncio.gather(
*[
self.async_get_category_repositories(HacsCategory(category))
for category in self.hacs.common.categories or []
]
)
async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
try:
repositories = await self.hacs.async_github_get_hacs_default_file(category)
except HacsException:
return
for repo in repositories:
if self.hacs.common.renamed_repositories.get(repo):
repo = self.hacs.common.renamed_repositories[repo]
if self.hacs.repositories.is_removed(repo):
continue
if repo in self.hacs.common.archived_repositories:
continue
repository = self.hacs.repositories.get_by_full_name(repo)
if repository is not None:
self.hacs.repositories.mark_default(repository)
if self.hacs.status.new and self.hacs.configuration.dev:
# Force update for new installations
self.hacs.queue.add(repository.common_update())
continue
self.hacs.queue.add(
self.hacs.async_register_repository(
repository_full_name=repo,
category=category,
default=True,
)
)

View File

@@ -0,0 +1,37 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsStage
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs update downloaded task."""
schedule = timedelta(hours=2)
stages = [HacsStage.STARTUP]
async def async_execute(self) -> None:
"""Execute the task."""
self.task_logger(
self.hacs.log.debug, "Starting recurring background task for installed repositories"
)
for repository in self.hacs.repositories.list_downloaded:
if repository.data.category in self.hacs.common.categories:
self.hacs.queue.add(repository.update_repository())
await self.hacs.data.async_write()
self.task_logger(
self.hacs.log.debug, "Recurring background task for installed repositories done"
)

View File

@@ -0,0 +1,60 @@
""""Hacs base setup task."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsCategory, HacsStage
from ..exceptions import HacsException
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Hacs update removed task."""
schedule = timedelta(hours=2)
stages = [HacsStage.STARTUP]
async def async_execute(self) -> None:
"""Execute the task."""
need_to_save = False
self.hacs.log.info("Loading removed repositories")
try:
removed_repositories = await self.hacs.async_github_get_hacs_default_file(
HacsCategory.REMOVED
)
except HacsException:
return
for item in removed_repositories:
removed = self.hacs.repositories.removed_repository(item["repository"])
removed.update_data(item)
for removed in self.hacs.repositories.list_removed:
if (repository := self.hacs.repositories.get_by_full_name(removed.repository)) is None:
continue
if repository.data.full_name in self.hacs.common.ignored_repositories:
continue
if repository.data.installed and removed.removal_type != "critical":
self.hacs.log.warning(
"You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. "
"Removal reason (%s)",
repository.data.full_name,
removed.reason,
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.hacs.data.async_write()

View File

@@ -0,0 +1,24 @@
""""Starting setup task: Verify API"."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from ..base import HacsBase
from ..enums import HacsStage
from .base import HacsTask
async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task:
"""Set up this task."""
return Task(hacs=hacs, hass=hass)
class Task(HacsTask):
"""Verify the connection to the GitHub API."""
stages = [HacsStage.SETUP]
async def async_execute(self) -> None:
"""Execute the task."""
can_update = await self.hacs.async_can_update()
self.task_logger(self.hacs.log.debug, f"Can update {can_update} repositories")