__init__.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. """
  2. HACS gives you a powerful UI to handle downloads of all your custom needs.
  3. For more details about this integration, please refer to the documentation at
  4. https://hacs.xyz/
  5. """
  6. from __future__ import annotations
  7. import os
  8. from typing import Any
  9. from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
  10. from aiogithubapi.const import ACCEPT_HEADERS
  11. from awesomeversion import AwesomeVersion
  12. from homeassistant.components.lovelace.system_health import system_health_info
  13. from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
  14. from homeassistant.const import Platform, __version__ as HAVERSION
  15. from homeassistant.core import HomeAssistant
  16. from homeassistant.helpers.aiohttp_client import async_get_clientsession
  17. from homeassistant.helpers.discovery import async_load_platform
  18. from homeassistant.helpers.event import async_call_later
  19. from homeassistant.helpers.start import async_at_start
  20. from homeassistant.loader import async_get_integration
  21. import voluptuous as vol
  22. from .base import HacsBase
  23. from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP
  24. from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode
  25. from .frontend import async_register_frontend
  26. from .utils.configuration_schema import hacs_config_combined
  27. from .utils.data import HacsData
  28. from .utils.queue_manager import QueueManager
  29. from .utils.version import version_left_higher_or_equal_then_right
  30. from .websocket import async_register_websocket_commands
  31. CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
  32. async def async_initialize_integration(
  33. hass: HomeAssistant,
  34. *,
  35. config_entry: ConfigEntry | None = None,
  36. config: dict[str, Any] | None = None,
  37. ) -> bool:
  38. """Initialize the integration"""
  39. hass.data[DOMAIN] = hacs = HacsBase()
  40. hacs.enable_hacs()
  41. if config is not None:
  42. if DOMAIN not in config:
  43. return True
  44. if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
  45. return True
  46. hacs.configuration.update_from_dict(
  47. {
  48. "config_type": ConfigurationType.YAML,
  49. **config[DOMAIN],
  50. "config": config[DOMAIN],
  51. }
  52. )
  53. if config_entry is not None:
  54. if config_entry.source == SOURCE_IMPORT:
  55. hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
  56. return False
  57. hacs.configuration.update_from_dict(
  58. {
  59. "config_entry": config_entry,
  60. "config_type": ConfigurationType.CONFIG_ENTRY,
  61. **config_entry.data,
  62. **config_entry.options,
  63. }
  64. )
  65. integration = await async_get_integration(hass, DOMAIN)
  66. hacs.set_stage(None)
  67. hacs.log.info(STARTUP, integration.version)
  68. clientsession = async_get_clientsession(hass)
  69. hacs.integration = integration
  70. hacs.version = integration.version
  71. hacs.configuration.dev = integration.version == "0.0.0"
  72. hacs.hass = hass
  73. hacs.queue = QueueManager(hass=hass)
  74. hacs.data = HacsData(hacs=hacs)
  75. hacs.system.running = True
  76. hacs.session = clientsession
  77. hacs.core.lovelace_mode = LovelaceMode.YAML
  78. try:
  79. lovelace_info = await system_health_info(hacs.hass)
  80. hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml"))
  81. except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
  82. # If this happens, the users YAML is not valid, we assume YAML mode
  83. pass
  84. hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
  85. hacs.core.config_path = hacs.hass.config.path()
  86. if hacs.core.ha_version is None:
  87. hacs.core.ha_version = AwesomeVersion(HAVERSION)
  88. ## Legacy GitHub client
  89. hacs.github = GitHub(
  90. hacs.configuration.token,
  91. clientsession,
  92. headers={
  93. "User-Agent": f"HACS/{hacs.version}",
  94. "Accept": ACCEPT_HEADERS["preview"],
  95. },
  96. )
  97. ## New GitHub client
  98. hacs.githubapi = GitHubAPI(
  99. token=hacs.configuration.token,
  100. session=clientsession,
  101. **{"client_name": f"HACS/{hacs.version}"},
  102. )
  103. async def async_startup():
  104. """HACS startup tasks."""
  105. hacs.enable_hacs()
  106. for location in (
  107. hass.config.path("custom_components/custom_updater.py"),
  108. hass.config.path("custom_components/custom_updater/__init__.py"),
  109. ):
  110. if os.path.exists(location):
  111. hacs.log.critical(
  112. "This cannot be used with custom_updater. "
  113. "To use this you need to remove custom_updater form %s",
  114. location,
  115. )
  116. hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
  117. return False
  118. if not version_left_higher_or_equal_then_right(
  119. hacs.core.ha_version.string,
  120. MINIMUM_HA_VERSION,
  121. ):
  122. hacs.log.critical(
  123. "You need HA version %s or newer to use this integration.",
  124. MINIMUM_HA_VERSION,
  125. )
  126. hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
  127. return False
  128. if not await hacs.data.restore():
  129. hacs.disable_hacs(HacsDisabledReason.RESTORE)
  130. return False
  131. can_update = await hacs.async_can_update()
  132. hacs.log.debug("Can update %s repositories", can_update)
  133. hacs.set_active_categories()
  134. async_register_websocket_commands(hass)
  135. async_register_frontend(hass, hacs)
  136. if hacs.configuration.config_type == ConfigurationType.YAML:
  137. hass.async_create_task(
  138. async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
  139. )
  140. hacs.log.info("Update entities are only supported when using UI configuration")
  141. else:
  142. hass.config_entries.async_setup_platforms(
  143. config_entry,
  144. [Platform.SENSOR, Platform.UPDATE]
  145. if hacs.configuration.experimental
  146. else [Platform.SENSOR],
  147. )
  148. hacs.set_stage(HacsStage.SETUP)
  149. if hacs.system.disabled:
  150. return False
  151. # Schedule startup tasks
  152. async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
  153. hacs.set_stage(HacsStage.WAITING)
  154. hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
  155. return not hacs.system.disabled
  156. async def async_try_startup(_=None):
  157. """Startup wrapper for yaml config."""
  158. try:
  159. startup_result = await async_startup()
  160. except AIOGitHubAPIException:
  161. startup_result = False
  162. if not startup_result:
  163. if (
  164. hacs.configuration.config_type == ConfigurationType.YAML
  165. or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
  166. ):
  167. hacs.log.info("Could not setup HACS, trying again in 15 min")
  168. async_call_later(hass, 900, async_try_startup)
  169. return
  170. hacs.enable_hacs()
  171. await async_try_startup()
  172. # Mischief managed!
  173. return True
  174. async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
  175. """Set up this integration using yaml."""
  176. return await async_initialize_integration(hass=hass, config=config)
  177. async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
  178. """Set up this integration using UI."""
  179. config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
  180. setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry)
  181. hacs: HacsBase = hass.data[DOMAIN]
  182. return setup_result and not hacs.system.disabled
  183. async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
  184. """Handle removal of an entry."""
  185. hacs: HacsBase = hass.data[DOMAIN]
  186. # Clear out pending queue
  187. hacs.queue.clear()
  188. for task in hacs.recuring_tasks:
  189. # Cancel all pending tasks
  190. task()
  191. # Store data
  192. await hacs.data.async_write(force=True)
  193. try:
  194. if hass.data.get("frontend_panels", {}).get("hacs"):
  195. hacs.log.info("Removing sidepanel")
  196. hass.components.frontend.async_remove_panel("hacs")
  197. except AttributeError:
  198. pass
  199. platforms = ["sensor"]
  200. if hacs.configuration.experimental:
  201. platforms.append("update")
  202. unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
  203. hacs.set_stage(None)
  204. hacs.disable_hacs(HacsDisabledReason.REMOVED)
  205. hass.data.pop(DOMAIN, None)
  206. return unload_ok
  207. async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
  208. """Reload the HACS config entry."""
  209. await async_unload_entry(hass, config_entry)
  210. await async_setup_entry(hass, config_entry)