integration.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """Class for integrations in HACS."""
  2. from __future__ import annotations
  3. from typing import TYPE_CHECKING, Any
  4. from homeassistant.helpers.issue_registry import async_create_issue, IssueSeverity
  5. from homeassistant.loader import async_get_custom_components
  6. from ..const import DOMAIN
  7. from ..enums import HacsCategory, HacsDispatchEvent, HacsGitHubRepo, RepositoryFile
  8. from ..exceptions import AddonRepositoryException, HacsException
  9. from ..utils.decode import decode_content
  10. from ..utils.decorator import concurrent
  11. from ..utils.filters import get_first_directory_in_directory
  12. from ..utils.json import json_loads
  13. from .base import HacsRepository
  14. if TYPE_CHECKING:
  15. from ..base import HacsBase
  16. class HacsIntegrationRepository(HacsRepository):
  17. """Integrations in HACS."""
  18. def __init__(self, hacs: HacsBase, full_name: str):
  19. """Initialize."""
  20. super().__init__(hacs=hacs)
  21. self.data.full_name = full_name
  22. self.data.full_name_lower = full_name.lower()
  23. self.data.category = HacsCategory.INTEGRATION
  24. self.content.path.remote = "custom_components"
  25. self.content.path.local = self.localpath
  26. @property
  27. def localpath(self):
  28. """Return localpath."""
  29. return f"{self.hacs.core.config_path}/custom_components/{self.data.domain}"
  30. async def async_post_installation(self):
  31. """Run post installation steps."""
  32. self.pending_restart = True
  33. if self.data.config_flow:
  34. if self.data.full_name != HacsGitHubRepo.INTEGRATION:
  35. await self.reload_custom_components()
  36. if self.data.first_install:
  37. self.pending_restart = False
  38. if self.pending_restart and self.hacs.configuration.experimental:
  39. self.logger.debug("%s Creating restart_required issue", self.string)
  40. async_create_issue(
  41. hass=self.hacs.hass,
  42. domain=DOMAIN,
  43. issue_id=f"restart_required_{self.data.id}_{self.ref}",
  44. is_fixable=True,
  45. issue_domain=self.data.domain or DOMAIN,
  46. severity=IssueSeverity.WARNING,
  47. translation_key="restart_required",
  48. translation_placeholders={
  49. "name": self.display_name,
  50. },
  51. )
  52. async def validate_repository(self):
  53. """Validate."""
  54. await self.common_validate()
  55. # Custom step 1: Validate content.
  56. if self.repository_manifest.content_in_root:
  57. self.content.path.remote = ""
  58. if self.content.path.remote == "custom_components":
  59. name = get_first_directory_in_directory(self.tree, "custom_components")
  60. if name is None:
  61. if (
  62. "repository.json" in self.treefiles
  63. or "repository.yaml" in self.treefiles
  64. or "repository.yml" in self.treefiles
  65. ):
  66. raise AddonRepositoryException()
  67. raise HacsException(
  68. f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant"
  69. )
  70. self.content.path.remote = f"custom_components/{name}"
  71. # Get the content of manifest.json
  72. if manifest := await self.async_get_integration_manifest():
  73. try:
  74. self.integration_manifest = manifest
  75. self.data.authors = manifest.get("codeowners", [])
  76. self.data.domain = manifest["domain"]
  77. self.data.manifest_name = manifest.get("name")
  78. self.data.config_flow = manifest.get("config_flow", False)
  79. except KeyError as exception:
  80. self.validate.errors.append(
  81. f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}"
  82. )
  83. self.hacs.log.error(
  84. "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
  85. )
  86. # Set local path
  87. self.content.path.local = self.localpath
  88. # Handle potential errors
  89. if self.validate.errors:
  90. for error in self.validate.errors:
  91. if not self.hacs.status.startup:
  92. self.logger.error("%s %s", self.string, error)
  93. return self.validate.success
  94. @concurrent(concurrenttasks=10, backoff_time=5)
  95. async def update_repository(self, ignore_issues=False, force=False):
  96. """Update."""
  97. if not await self.common_update(ignore_issues, force) and not force:
  98. return
  99. if self.repository_manifest.content_in_root:
  100. self.content.path.remote = ""
  101. if self.content.path.remote == "custom_components":
  102. name = get_first_directory_in_directory(self.tree, "custom_components")
  103. self.content.path.remote = f"custom_components/{name}"
  104. # Get the content of manifest.json
  105. if manifest := await self.async_get_integration_manifest():
  106. try:
  107. self.integration_manifest = manifest
  108. self.data.authors = manifest.get("codeowners", [])
  109. self.data.domain = manifest["domain"]
  110. self.data.manifest_name = manifest.get("name")
  111. self.data.config_flow = manifest.get("config_flow", False)
  112. except KeyError as exception:
  113. self.validate.errors.append(
  114. f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}"
  115. )
  116. self.hacs.log.error(
  117. "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
  118. )
  119. # Set local path
  120. self.content.path.local = self.localpath
  121. # Signal entities to refresh
  122. if self.data.installed:
  123. self.hacs.async_dispatch(
  124. HacsDispatchEvent.REPOSITORY,
  125. {
  126. "id": 1337,
  127. "action": "update",
  128. "repository": self.data.full_name,
  129. "repository_id": self.data.id,
  130. },
  131. )
  132. async def reload_custom_components(self):
  133. """Reload custom_components (and config flows)in HA."""
  134. self.logger.info("Reloading custom_component cache")
  135. del self.hacs.hass.data["custom_components"]
  136. await async_get_custom_components(self.hacs.hass)
  137. self.logger.info("Custom_component cache reloaded")
  138. async def async_get_integration_manifest(self, ref: str = None) -> dict[str, Any] | None:
  139. """Get the content of the manifest.json file."""
  140. manifest_path = (
  141. "manifest.json"
  142. if self.repository_manifest.content_in_root
  143. else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}"
  144. )
  145. if not manifest_path in (x.full_path for x in self.tree):
  146. raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'")
  147. response = await self.hacs.async_github_api_method(
  148. method=self.hacs.githubapi.repos.contents.get,
  149. repository=self.data.full_name,
  150. path=manifest_path,
  151. **{"params": {"ref": ref or self.version_to_download()}},
  152. )
  153. if response:
  154. return json_loads(decode_content(response.data.content))