python
This commit is contained in:
21
custom_components/hacs/repositories/__init__.py
Normal file
21
custom_components/hacs/repositories/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Initialize repositories."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..enums import HacsCategory
|
||||
from .appdaemon import HacsAppdaemonRepository
|
||||
from .base import HacsRepository
|
||||
from .integration import HacsIntegrationRepository
|
||||
from .plugin import HacsPluginRepository
|
||||
from .python_script import HacsPythonScriptRepository
|
||||
from .template import HacsTemplateRepository
|
||||
from .theme import HacsThemeRepository
|
||||
|
||||
REPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = {
|
||||
HacsCategory.THEME: HacsThemeRepository,
|
||||
HacsCategory.INTEGRATION: HacsIntegrationRepository,
|
||||
HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository,
|
||||
HacsCategory.APPDAEMON: HacsAppdaemonRepository,
|
||||
HacsCategory.PLUGIN: HacsPluginRepository,
|
||||
HacsCategory.TEMPLATE: HacsTemplateRepository,
|
||||
}
|
||||
93
custom_components/hacs/repositories/appdaemon.py
Normal file
93
custom_components/hacs/repositories/appdaemon.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Class for appdaemon apps in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogithubapi import AIOGitHubAPIException
|
||||
|
||||
from ..enums import HacsCategory, HacsDispatchEvent
|
||||
from ..exceptions import HacsException
|
||||
from ..utils.decorator import concurrent
|
||||
from .base import HacsRepository
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsAppdaemonRepository(HacsRepository):
|
||||
"""Appdaemon apps in HACS."""
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.category = HacsCategory.APPDAEMON
|
||||
self.content.path.local = self.localpath
|
||||
self.content.path.remote = "apps"
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.name}"
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
try:
|
||||
addir = await self.repository_object.get_contents("apps", self.ref)
|
||||
except AIOGitHubAPIException:
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
) from None
|
||||
|
||||
if not isinstance(addir, list):
|
||||
self.validate.errors.append(f"{self.string} Repository structure not compliant")
|
||||
|
||||
self.content.path.remote = addir[0].path
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
# Get appdaemon objects.
|
||||
if self.repository_manifest:
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "apps":
|
||||
addir = await self.repository_object.get_contents(self.content.path.remote, self.ref)
|
||||
self.content.path.remote = addir[0].path
|
||||
self.content.objects = await self.repository_object.get_contents(
|
||||
self.content.path.remote, self.ref
|
||||
)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
1454
custom_components/hacs/repositories/base.py
Normal file
1454
custom_components/hacs/repositories/base.py
Normal file
File diff suppressed because it is too large
Load Diff
217
custom_components/hacs/repositories/integration.py
Normal file
217
custom_components/hacs/repositories/integration.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""Class for integrations in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.loader import async_get_custom_components
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..enums import HacsCategory, HacsDispatchEvent, HacsGitHubRepo, RepositoryFile
|
||||
from ..exceptions import AddonRepositoryException, HacsException
|
||||
from ..utils.decode import decode_content
|
||||
from ..utils.decorator import concurrent
|
||||
from ..utils.filters import get_first_directory_in_directory
|
||||
from ..utils.json import json_loads
|
||||
from .base import HacsRepository
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsIntegrationRepository(HacsRepository):
|
||||
"""Integrations in HACS."""
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.category = HacsCategory.INTEGRATION
|
||||
self.content.path.remote = "custom_components"
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/custom_components/{self.data.domain}"
|
||||
|
||||
async def async_post_installation(self):
|
||||
"""Run post installation steps."""
|
||||
self.pending_restart = True
|
||||
if self.data.config_flow:
|
||||
if self.data.full_name != HacsGitHubRepo.INTEGRATION:
|
||||
await self.reload_custom_components()
|
||||
if self.data.first_install:
|
||||
self.pending_restart = False
|
||||
|
||||
if self.pending_restart:
|
||||
self.logger.debug("%s Creating restart_required issue", self.string)
|
||||
async_create_issue(
|
||||
hass=self.hacs.hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"restart_required_{self.data.id}_{self.ref}",
|
||||
is_fixable=True,
|
||||
issue_domain=self.data.domain or DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="restart_required",
|
||||
translation_placeholders={
|
||||
"name": self.display_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_post_uninstall(self) -> None:
|
||||
"""Run post uninstall steps."""
|
||||
if self.data.config_flow:
|
||||
await self.reload_custom_components()
|
||||
else:
|
||||
self.pending_restart = True
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
name = get_first_directory_in_directory(self.tree, "custom_components")
|
||||
if name is None:
|
||||
if (
|
||||
"repository.json" in self.treefiles
|
||||
or "repository.yaml" in self.treefiles
|
||||
or "repository.yml" in self.treefiles
|
||||
):
|
||||
raise AddonRepositoryException()
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {
|
||||
self.ref.replace('tags/', '')} is not compliant"
|
||||
)
|
||||
self.content.path.remote = f"custom_components/{name}"
|
||||
|
||||
# Get the content of manifest.json
|
||||
if manifest := await self.async_get_integration_manifest():
|
||||
try:
|
||||
self.integration_manifest = manifest
|
||||
self.data.authors = manifest.get("codeowners", [])
|
||||
self.data.domain = manifest["domain"]
|
||||
self.data.manifest_name = manifest.get("name")
|
||||
self.data.config_flow = manifest.get("config_flow", False)
|
||||
|
||||
except KeyError as exception:
|
||||
self.validate.errors.append(
|
||||
f"Missing expected key '{exception}' in {
|
||||
RepositoryFile.MAINIFEST_JSON}"
|
||||
)
|
||||
self.hacs.log.error(
|
||||
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
|
||||
)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
if self.content.path.remote == "custom_components":
|
||||
name = get_first_directory_in_directory(self.tree, "custom_components")
|
||||
self.content.path.remote = f"custom_components/{name}"
|
||||
|
||||
# Get the content of manifest.json
|
||||
if manifest := await self.async_get_integration_manifest():
|
||||
try:
|
||||
self.integration_manifest = manifest
|
||||
self.data.authors = manifest.get("codeowners", [])
|
||||
self.data.domain = manifest["domain"]
|
||||
self.data.manifest_name = manifest.get("name")
|
||||
self.data.config_flow = manifest.get("config_flow", False)
|
||||
|
||||
except KeyError as exception:
|
||||
self.validate.errors.append(
|
||||
f"Missing expected key '{exception}' in {
|
||||
RepositoryFile.MAINIFEST_JSON}"
|
||||
)
|
||||
self.hacs.log.error(
|
||||
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
|
||||
)
|
||||
|
||||
# Set local path
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
async def reload_custom_components(self):
|
||||
"""Reload custom_components (and config flows)in HA."""
|
||||
self.logger.info("Reloading custom_component cache")
|
||||
del self.hacs.hass.data["custom_components"]
|
||||
await async_get_custom_components(self.hacs.hass)
|
||||
self.logger.info("Custom_component cache reloaded")
|
||||
|
||||
async def async_get_integration_manifest(self, ref: str = None) -> dict[str, Any] | None:
|
||||
"""Get the content of the manifest.json file."""
|
||||
manifest_path = (
|
||||
"manifest.json"
|
||||
if self.repository_manifest.content_in_root
|
||||
else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}"
|
||||
)
|
||||
|
||||
if not manifest_path in (x.full_path for x in self.tree):
|
||||
raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'")
|
||||
|
||||
response = await self.hacs.async_github_api_method(
|
||||
method=self.hacs.githubapi.repos.contents.get,
|
||||
repository=self.data.full_name,
|
||||
path=manifest_path,
|
||||
**{"params": {"ref": ref or self.version_to_download()}},
|
||||
)
|
||||
if response:
|
||||
return json_loads(decode_content(response.data.content))
|
||||
|
||||
async def get_integration_manifest(self, *, version: str, **kwargs) -> dict[str, Any] | None:
|
||||
"""Get the content of the manifest.json file."""
|
||||
manifest_path = (
|
||||
"manifest.json"
|
||||
if self.repository_manifest.content_in_root
|
||||
else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}"
|
||||
)
|
||||
|
||||
if manifest_path not in (x.full_path for x in self.tree):
|
||||
raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'")
|
||||
|
||||
self.logger.debug("%s Getting manifest.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}/{manifest_path}",
|
||||
nolog=True,
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
return json_loads(result)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return None
|
||||
246
custom_components/hacs/repositories/plugin.py
Normal file
246
custom_components/hacs/repositories/plugin.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Class for plugins in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..enums import HacsCategory, HacsDispatchEvent
|
||||
from ..exceptions import HacsException
|
||||
from ..utils.decorator import concurrent
|
||||
from ..utils.json import json_loads
|
||||
from .base import HacsRepository
|
||||
|
||||
HACSTAG_REPLACER = re.compile(r"\D+")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.lovelace.resources import ResourceStorageCollection
|
||||
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsPluginRepository(HacsRepository):
|
||||
"""Plugins in HACS."""
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.file_name = None
|
||||
self.data.category = HacsCategory.PLUGIN
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/www/community/{self.data.full_name.split('/')[-1]}"
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
self.update_filenames()
|
||||
|
||||
if self.content.path.remote is None:
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
if self.content.path.remote == "release":
|
||||
self.content.single = True
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
async def async_post_installation(self):
|
||||
"""Run post installation steps."""
|
||||
await self.hacs.async_setup_frontend_endpoint_plugin()
|
||||
await self.update_dashboard_resources()
|
||||
|
||||
async def async_post_uninstall(self):
|
||||
"""Run post uninstall steps."""
|
||||
await self.remove_dashboard_resources()
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
# Get plugin objects.
|
||||
self.update_filenames()
|
||||
|
||||
if self.content.path.remote is None:
|
||||
self.validate.errors.append(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
if self.content.path.remote == "release":
|
||||
self.content.single = True
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
async def get_package_content(self):
|
||||
"""Get package content."""
|
||||
try:
|
||||
package = await self.repository_object.get_contents("package.json", self.ref)
|
||||
package = json_loads(package.content)
|
||||
|
||||
if package:
|
||||
self.data.authors = package["author"]
|
||||
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
def update_filenames(self) -> None:
|
||||
"""Get the filename to target."""
|
||||
content_in_root = self.repository_manifest.content_in_root
|
||||
if specific_filename := self.repository_manifest.filename:
|
||||
valid_filenames = (specific_filename,)
|
||||
else:
|
||||
valid_filenames = (
|
||||
f"{self.data.name.replace('lovelace-', '')}.js",
|
||||
f"{self.data.name}.js",
|
||||
f"{self.data.name}.umd.js",
|
||||
f"{self.data.name}-bundle.js",
|
||||
)
|
||||
|
||||
if not content_in_root:
|
||||
if self.releases.objects:
|
||||
release = self.releases.objects[0]
|
||||
if release.assets:
|
||||
if assetnames := [
|
||||
filename
|
||||
for filename in valid_filenames
|
||||
for asset in release.assets
|
||||
if filename == asset.name
|
||||
]:
|
||||
self.data.file_name = assetnames[0]
|
||||
self.content.path.remote = "release"
|
||||
return
|
||||
|
||||
all_paths = {x.full_path for x in self.tree}
|
||||
for filename in valid_filenames:
|
||||
if filename in all_paths:
|
||||
self.data.file_name = filename
|
||||
self.content.path.remote = ""
|
||||
return
|
||||
if not content_in_root and f"dist/{filename}" in all_paths:
|
||||
self.data.file_name = filename.split("/")[-1]
|
||||
self.content.path.remote = "dist"
|
||||
return
|
||||
|
||||
def generate_dashboard_resource_hacstag(self) -> str:
|
||||
"""Get the HACS tag used by dashboard resources."""
|
||||
version = (
|
||||
self.display_installed_version
|
||||
or self.data.selected_tag
|
||||
or self.display_available_version
|
||||
)
|
||||
return f"{self.data.id}{HACSTAG_REPLACER.sub('', version)}"
|
||||
|
||||
def generate_dashboard_resource_namespace(self) -> str:
|
||||
"""Get the dashboard resource namespace."""
|
||||
return f"/hacsfiles/{self.data.full_name.split("/")[1]}"
|
||||
|
||||
def generate_dashboard_resource_url(self) -> str:
|
||||
"""Get the dashboard resource namespace."""
|
||||
filename = self.data.file_name
|
||||
if "/" in filename:
|
||||
self.logger.warning("%s have defined an invalid file name %s", self.string, filename)
|
||||
filename = filename.split("/")[-1]
|
||||
return (
|
||||
f"{self.generate_dashboard_resource_namespace()}/{filename}"
|
||||
f"?hacstag={self.generate_dashboard_resource_hacstag()}"
|
||||
)
|
||||
|
||||
def _get_resource_handler(self) -> ResourceStorageCollection | None:
|
||||
"""Get the resource handler."""
|
||||
resources: ResourceStorageCollection | None
|
||||
if not (hass_data := self.hacs.hass.data):
|
||||
self.logger.error("%s Can not access the hass data", self.string)
|
||||
return
|
||||
|
||||
if (lovelace_data := hass_data.get("lovelace")) is None:
|
||||
self.logger.warning("%s Can not access the lovelace integration data", self.string)
|
||||
return
|
||||
|
||||
if self.hacs.core.ha_version > "2025.1.99":
|
||||
# Changed to 2025.2.0
|
||||
# Changed in https://github.com/home-assistant/core/pull/136313
|
||||
resources = lovelace_data.resources
|
||||
else:
|
||||
resources = lovelace_data.get("resources")
|
||||
|
||||
if resources is None:
|
||||
self.logger.warning("%s Can not access the dashboard resources", self.string)
|
||||
return
|
||||
|
||||
if not hasattr(resources, "store") or resources.store is None:
|
||||
self.logger.info("%s YAML mode detected, can not update resources", self.string)
|
||||
return
|
||||
|
||||
if resources.store.key != "lovelace_resources" or resources.store.version != 1:
|
||||
self.logger.warning("%s Can not use the dashboard resources", self.string)
|
||||
return
|
||||
|
||||
return resources
|
||||
|
||||
async def update_dashboard_resources(self) -> None:
|
||||
"""Update dashboard resources."""
|
||||
if not (resources := self._get_resource_handler()):
|
||||
return
|
||||
|
||||
if not resources.loaded:
|
||||
await resources.async_load()
|
||||
|
||||
namespace = self.generate_dashboard_resource_namespace()
|
||||
url = self.generate_dashboard_resource_url()
|
||||
|
||||
for entry in resources.async_items():
|
||||
if (entry_url := entry["url"]).startswith(namespace):
|
||||
if entry_url != url:
|
||||
self.logger.info(
|
||||
"%s Updating existing dashboard resource from %s to %s",
|
||||
self.string,
|
||||
entry_url,
|
||||
url,
|
||||
)
|
||||
await resources.async_update_item(entry["id"], {"url": url})
|
||||
return
|
||||
|
||||
# Nothing was updated, add the resource
|
||||
self.logger.info("%s Adding dashboard resource %s", self.string, url)
|
||||
await resources.async_create_item({"res_type": "module", "url": url})
|
||||
|
||||
async def remove_dashboard_resources(self) -> None:
|
||||
"""Remove dashboard resources."""
|
||||
if not (resources := self._get_resource_handler()):
|
||||
return
|
||||
|
||||
if not resources.loaded:
|
||||
await resources.async_load()
|
||||
|
||||
namespace = self.generate_dashboard_resource_namespace()
|
||||
|
||||
for entry in resources.async_items():
|
||||
if entry["url"].startswith(namespace):
|
||||
self.logger.info("%s Removing dashboard resource %s", self.string, entry["url"])
|
||||
await resources.async_delete_item(entry["id"])
|
||||
return
|
||||
111
custom_components/hacs/repositories/python_script.py
Normal file
111
custom_components/hacs/repositories/python_script.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Class for python_scripts in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..enums import HacsCategory, HacsDispatchEvent
|
||||
from ..exceptions import HacsException
|
||||
from ..utils.decorator import concurrent
|
||||
from .base import HacsRepository
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsPythonScriptRepository(HacsRepository):
|
||||
"""python_scripts in HACS."""
|
||||
|
||||
category = "python_script"
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.category = HacsCategory.PYTHON_SCRIPT
|
||||
self.content.path.remote = "python_scripts"
|
||||
self.content.path.local = self.localpath
|
||||
self.content.single = True
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/python_scripts"
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
compliant = False
|
||||
for treefile in self.treefiles:
|
||||
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"):
|
||||
compliant = True
|
||||
break
|
||||
if not compliant:
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
async def async_post_registration(self):
|
||||
"""Registration."""
|
||||
# Set name
|
||||
self.update_filenames()
|
||||
|
||||
if self.hacs.system.action:
|
||||
await self.hacs.validation.async_run_repository_checks(self)
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
# Get python_script objects.
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
compliant = False
|
||||
for treefile in self.treefiles:
|
||||
if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"):
|
||||
compliant = True
|
||||
break
|
||||
if not compliant:
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
# Update name
|
||||
self.update_filenames()
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
def update_filenames(self) -> None:
|
||||
"""Get the filename to target."""
|
||||
for treefile in self.tree:
|
||||
if treefile.full_path.startswith(
|
||||
self.content.path.remote
|
||||
) and treefile.full_path.endswith(".py"):
|
||||
self.data.file_name = treefile.filename
|
||||
106
custom_components/hacs/repositories/template.py
Normal file
106
custom_components/hacs/repositories/template.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Class for themes in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from ..enums import HacsCategory, HacsDispatchEvent
|
||||
from ..exceptions import HacsException
|
||||
from ..utils.decorator import concurrent
|
||||
from .base import HacsRepository
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsTemplateRepository(HacsRepository):
|
||||
"""Custom templates in HACS."""
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.category = HacsCategory.TEMPLATE
|
||||
self.content.path.remote = ""
|
||||
self.content.path.local = self.localpath
|
||||
self.content.single = True
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/custom_templates"
|
||||
|
||||
async def async_post_installation(self):
|
||||
"""Run post installation steps."""
|
||||
await self._reload_custom_templates()
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
self.data.file_name = self.repository_manifest.filename
|
||||
|
||||
if (
|
||||
not self.data.file_name
|
||||
or "/" in self.data.file_name
|
||||
or not self.data.file_name.endswith(".jinja")
|
||||
or self.data.file_name not in self.treefiles
|
||||
):
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
async def async_post_registration(self):
|
||||
"""Registration."""
|
||||
# Set filenames
|
||||
self.data.file_name = self.repository_manifest.filename
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
if self.hacs.system.action:
|
||||
await self.hacs.validation.async_run_repository_checks(self)
|
||||
|
||||
async def async_post_uninstall(self) -> None:
|
||||
"""Run post uninstall steps."""
|
||||
await self._reload_custom_templates()
|
||||
|
||||
async def _reload_custom_templates(self) -> None:
|
||||
"""Reload custom templates."""
|
||||
self.logger.debug("%s Reloading custom templates", self.string)
|
||||
try:
|
||||
await self.hacs.hass.services.async_call("homeassistant", "reload_custom_templates", {})
|
||||
except HomeAssistantError as exception:
|
||||
self.logger.exception("%s %s", self.string, exception)
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
# Update filenames
|
||||
self.data.file_name = self.repository_manifest.filename
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
119
custom_components/hacs/repositories/theme.py
Normal file
119
custom_components/hacs/repositories/theme.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Class for themes in HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from ..enums import HacsCategory, HacsDispatchEvent
|
||||
from ..exceptions import HacsException
|
||||
from ..utils.decorator import concurrent
|
||||
from .base import HacsRepository
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..base import HacsBase
|
||||
|
||||
|
||||
class HacsThemeRepository(HacsRepository):
|
||||
"""Themes in HACS."""
|
||||
|
||||
def __init__(self, hacs: HacsBase, full_name: str):
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
self.data.full_name = full_name
|
||||
self.data.full_name_lower = full_name.lower()
|
||||
self.data.category = HacsCategory.THEME
|
||||
self.content.path.remote = "themes"
|
||||
self.content.path.local = self.localpath
|
||||
self.content.single = False
|
||||
|
||||
@property
|
||||
def localpath(self):
|
||||
"""Return localpath."""
|
||||
return f"{self.hacs.core.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"
|
||||
|
||||
async def async_post_installation(self):
|
||||
"""Run post installation steps."""
|
||||
await self._reload_frontend_themes()
|
||||
|
||||
async def validate_repository(self):
|
||||
"""Validate."""
|
||||
# Run common validation steps.
|
||||
await self.common_validate()
|
||||
|
||||
# Custom step 1: Validate content.
|
||||
compliant = False
|
||||
for treefile in self.treefiles:
|
||||
if treefile.startswith("themes/") and treefile.endswith(".yaml"):
|
||||
compliant = True
|
||||
break
|
||||
if not compliant:
|
||||
raise HacsException(
|
||||
f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
|
||||
)
|
||||
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
# Handle potential errors
|
||||
if self.validate.errors:
|
||||
for error in self.validate.errors:
|
||||
if not self.hacs.status.startup:
|
||||
self.logger.error("%s %s", self.string, error)
|
||||
return self.validate.success
|
||||
|
||||
async def async_post_registration(self):
|
||||
"""Registration."""
|
||||
# Set name
|
||||
self.update_filenames()
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
if self.hacs.system.action:
|
||||
await self.hacs.validation.async_run_repository_checks(self)
|
||||
|
||||
async def _reload_frontend_themes(self) -> None:
|
||||
"""Reload frontend themes."""
|
||||
self.logger.debug("%s Reloading frontend themes", self.string)
|
||||
try:
|
||||
await self.hacs.hass.services.async_call("frontend", "reload_themes", {})
|
||||
except HomeAssistantError as exception:
|
||||
self.logger.exception("%s %s", self.string, exception)
|
||||
|
||||
async def async_post_uninstall(self) -> None:
|
||||
"""Run post uninstall steps."""
|
||||
await self._reload_frontend_themes()
|
||||
|
||||
@concurrent(concurrenttasks=10, backoff_time=5)
|
||||
async def update_repository(self, ignore_issues=False, force=False):
|
||||
"""Update."""
|
||||
if not await self.common_update(ignore_issues, force) and not force:
|
||||
return
|
||||
|
||||
# Get theme objects.
|
||||
if self.repository_manifest.content_in_root:
|
||||
self.content.path.remote = ""
|
||||
|
||||
# Update name
|
||||
self.update_filenames()
|
||||
self.content.path.local = self.localpath
|
||||
|
||||
# Signal frontend to refresh
|
||||
if self.data.installed:
|
||||
self.hacs.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": self.data.full_name,
|
||||
"repository_id": self.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
def update_filenames(self) -> None:
|
||||
"""Get the filename to target."""
|
||||
for treefile in self.tree:
|
||||
if treefile.full_path.startswith(
|
||||
self.content.path.remote
|
||||
) and treefile.full_path.endswith(".yaml"):
|
||||
self.data.file_name = treefile.filename
|
||||
Reference in New Issue
Block a user