def dummy_repository_base(repository=None): if repository is None: repository = HacsRepository() repository.hacs.hass = HomeAssistant() repository.hacs.hass.data = {"custom_components": []} repository.logger = Logger("hacs.test.test") repository.data.full_name = "test/test" repository.versions.available = "3" repository.status.selected_tag = "3" repository.ref = version_to_install(repository) repository.integration_manifest = {"config_flow": False, "domain": "test"} repository.releases.published_tags = ["1", "2", "3"] repository.data.update_data(repository_data) return repository
async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name ) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set description if self.repository_object.description: self.information.description = self.repository_object.description
async def update_data(self): """Update data.""" Logger("custom_components.healthchecksio").debug("Running update") # This is where the main logic to update platform data goes. try: verify_ssl = not self.self_hosted or self.site_root.startswith( "https") session = async_get_clientsession(self.hass, verify_ssl) headers = {"X-Api-Key": self.api_key} async with async_timeout.timeout(10): data = await session.get(f"{self.site_root}/api/v1/checks/", headers=headers) self.hass.data[DOMAIN_DATA]["data"] = await data.json() if self.self_hosted: check_url = f"{self.site_root}/{self.ping_endpoint}/{self.check}" else: check_url = f"https://hc-ping.com/{self.check}" await asyncio.sleep(1) # needed for self-hosted instances await session.get(check_url) except Exception as error: # pylint: disable=broad-except Logger("custom_components.healthchecksio").error( f"Could not update data - {error}")
async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Update default branch self.information.default_branch = self.repository_object.default_branch # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Update last updaeted self.information.last_updated = self.repository_object.pushed_at # Update topics self.information.topics = self.repository_object.topics # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" await self.get_info_md_content() # Update releases await self.get_releases()
async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Set repository name self.information.name = self.information.full_name.split("/")[1]
def internet_connectivity_check(host="api.github.com"): """Verify network connectivity.""" logger = Logger("hacs.network.check") try: result = ping(host, count=1, timeout=3) if result.success(): logger.info("All good") return True except gaierror: logger.error(f"DNS issues, could not resolve {host}") return False
async def async_save_file(location, content): """Save files.""" logger = Logger("hacs.download.save") logger.debug(f"Saving {location}") mode = "w" encoding = "utf-8" errors = "ignore" if not isinstance(content, str): mode = "wb" encoding = None errors = None try: async with aiofiles.open(location, mode=mode, encoding=encoding, errors=errors) as outfile: await outfile.write(content) outfile.close() # Create gz for .js files if os.path.isfile(location): if location.endswith(".js") or location.endswith(".css"): with open(location, "rb") as f_in: with gzip.open(location + ".gz", "wb") as f_out: shutil.copyfileobj(f_in, f_out) # Remove with 2.0 if "themes" in location and location.endswith(".yaml"): filename = location.split("/")[-1] base = location.split("/themes/")[0] combined = f"{base}/themes/{filename}" if os.path.exists(combined): logger.info(f"Removing old theme file {combined}") os.remove(combined) except Exception as error: # pylint: disable=broad-except msg = "Could not write data to {} - {}".format(location, error) logger.error(msg) return False return os.path.exists(location)
async def check_files(hass): """Return bool that indicates if all files are present.""" # Verify that the user downloaded all files. base = f"{hass.config.path()}/custom_components/{DOMAIN}/" missing = [] for file in REQUIRED_FILES: fullpath = "{}{}".format(base, file) if not os.path.exists(fullpath): missing.append(file) if missing: Logger("custom_components.healthchecksio").critical( f"The following files are missing: {missing}") returnvalue = False else: returnvalue = True return returnvalue
async def service_generate(call): """Generate the files.""" base = hass.config.path() if ReadmeConfiguration.convert_lovelace: convert_lovelace(hass) custom_components = get_custom_components(hass) variables = { "custom_components": custom_components, "states": AllStates(hass) } with open(f"{base}/templates/README.j2", "r") as readme: content = readme.read() template = Template(content) try: render = template.render(variables) with open(f"{base}/README.md", "w") as out_file: out_file.write(render) except Exception as exception: Logger("custom_components.readme").error(exception)
class Backup: """Backup.""" def __init__(self, local_path): """initialize.""" self.logger = Logger("hacs.backup") self.local_path = local_path self.backup_path = "/tmp/hacs_backup" def create(self): """Create a backup in /tmp""" if os.path.exists(self.backup_path): rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) os.makedirs(self.backup_path, exist_ok=True) try: if os.path.isfile(self.local_path): copy2(self.local_path, self.backup_path) os.remove(self.local_path) else: copy_tree(self.local_path, self.backup_path) rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) self.logger.debug( f"Backup for {self.local_path}, created in {self.backup_path}") except Exception: # pylint: disable=broad-except pass def restore(self): """Restore from backup.""" if os.path.isfile(self.local_path): os.remove(self.local_path) else: rmtree(self.local_path) while os.path.exists(self.local_path): sleep(0.1) copy2(self.backup_path, self.local_path) self.logger.debug( f"Restored {self.local_path}, from backup {self.backup_path}") def cleanup(self): """Cleanup backup files.""" rmtree(self.backup_path) while os.path.exists(self.backup_path): sleep(0.1) self.logger.debug(f"Backup dir {self.backup_path} cleared")
def render_template(content, context): """Render templates in content.""" # Fix None issues if context.releases.last_release_object is not None: prerelease = context.releases.last_release_object.prerelease else: prerelease = False # Render the template try: render = Template(content) render = render.render( installed=context.data.installed, pending_update=context.pending_upgrade, prerelease=prerelease, selected_tag=context.data.selected_tag, version_available=context.releases.last_release, version_installed=context.display_installed_version, ) return render except Exception as exception: Logger("hacs.template").debug(exception) return content
async def async_setup_entry(hass, config_entry): """Set up this integration using UI.""" # Print startup message Logger("custom_components.healthchecksio").info( CC_STARTUP_VERSION.format(name=DOMAIN, version=INTEGRATION_VERSION, issue_link=ISSUE_URL)) # Check that all required files are present file_check = await check_files(hass) if not file_check: return False # Create DATA dict if DOMAIN_DATA not in hass.data: hass.data[DOMAIN_DATA] = {} if "data" not in hass.data[DOMAIN_DATA]: hass.data[DOMAIN_DATA] = {} # Get "global" configuration. api_key = config_entry.data.get("api_key") check = config_entry.data.get("check") self_hosted = config_entry.data.get("self_hosted") site_root = config_entry.data.get("site_root") ping_endpoint = config_entry.data.get("ping_endpoint") # Configure the client. hass.data[DOMAIN_DATA]["client"] = HealthchecksioData( hass, api_key, check, self_hosted, site_root, ping_endpoint) # Add binary_sensor hass.async_add_job( hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")) return True
class HacsAPI(HacsWebResponse): """HacsAPI class.""" name = "hacsapi" def __init__(self): """Initialize.""" self.logger = Logger("hacs.api") self.url = self.hacsapi + "/{endpoint}" async def post(self, request, endpoint): # pylint: disable=unused-argument """Handle HACS API requests.""" if self.system.disabled: return web.Response(status=404) self.endpoint = endpoint self.postdata = await request.post() self.raw_headers = request.raw_headers self.request = request self.logger.debug(f"Endpoint ({endpoint}) called") if self.configuration.dev: self.logger.debug(f"Raw headers ({self.raw_headers})") self.logger.debug(f"Postdata ({self.postdata})") if self.endpoint in APIRESPONSE: try: response = APIRESPONSE[self.endpoint] response = await response.response(self) except Exception as exception: render = self.render(f"error", message=exception) return web.Response( body=render, content_type="text/html", charset="utf-8" ) else: # Return default response. response = await APIRESPONSE["generic"].response(self) # set headers response.headers["Cache-Control"] = "max-age=0, must-revalidate" # serve the response return response
async def get_default_repos_orgs(github: type(AIOGitHub), category: str) -> dict: """Gets default org repositories.""" repositories = [] logger = Logger("hacs") orgs = { "plugin": "custom-cards", "integration": "custom-components", "theme": "home-assistant-community-themes", } if category not in orgs: logger.error(f"{category} is not in {str(orgs.keys())}") return try: repos = await github.get_org_repos(orgs[category]) for repo in repos: repositories.append(repo.full_name) except AIOGitHubException as exception: logger.error(exception) return repositories
class Hacs: """The base class of HACS, nested thoughout the project.""" token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}" hacsweb = f"/hacsweb/{token}" hacsapi = f"/hacsapi/{token}" repositories = [] repo = None developer = Developer() data = None configuration = None logger = Logger("hacs") github = None hass = None version = None system = System() tasks = [] common = HacsCommon() def get_by_id(self, repository_id): """Get repository by ID.""" try: for repository in self.repositories: if repository.information.uid == repository_id: return repository except Exception: # pylint: disable=broad-except pass return None def get_by_name(self, repository_full_name): """Get repository by full_name.""" try: for repository in self.repositories: if repository.information.full_name == repository_full_name: return repository except Exception: # pylint: disable=broad-except pass return None def is_known(self, repository_full_name): """Return a bool if the repository is known.""" for repository in self.repositories: if repository.information.full_name == repository_full_name: return True return False @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.information.full_name) async def register_repository(self, full_name, category, check=True): """Register a repository.""" from ..repositories.repository import RERPOSITORY_CLASSES if full_name in self.common.skip: self.logger.debug(f"Skipping {full_name}") return if category not in RERPOSITORY_CLASSES: self.logger.error(f"{category} is not a valid repository category.") return False repository = RERPOSITORY_CLASSES[category](full_name) if check: try: await repository.registration() if repository.validate.errors: self.common.skip.append(repository.information.full_name) if not self.system.status.startup: self.logger.error(f"Validation for {full_name} failed.") return repository.validate.errors repository.logger.info("Registration complete") except AIOGitHubException as exception: self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) self.common.skip.append(repository.information.full_name) if not self.system.status.startup: self.logger.error( f"Validation for {full_name} failed with {exception}." ) return self.hass.bus.fire( "hacs/repository", { "id": 1337, "action": "registration", "repository": repository.information.full_name, }, ) self.repositories.append(repository) async def startup_tasks(self): """Tasks tha are started after startup.""" self.system.status.background_task = True self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) await self.load_known_repositories() self.clear_out_blacklisted_repositories() self.tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_installed, timedelta(minutes=30) ) ) self.tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_all, timedelta(minutes=800) ) ) self.system.status.startup = False self.system.status.background_task = False self.data.write() async def recuring_tasks_installed(self, notarealarg=None): """Recuring tasks for installed repositories.""" self.logger.debug( "Starting recuring background task for installed repositories" ) self.system.status.background_task = True self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: if repository.status.installed: try: await repository.update_repository() repository.logger.debug("Information update done.") except AIOGitHubException: self.system.status.background_task = False self.data.write() self.logger.debug( "Recuring background task for installed repositories done" ) return self.system.status.background_task = False self.data.write() self.logger.debug("Recuring background task for installed repositories done") async def recuring_tasks_all(self, notarealarg=None): """Recuring tasks for all repositories.""" self.logger.debug("Starting recuring background task for all repositories") self.system.status.background_task = True self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: try: await repository.update_repository() repository.logger.debug("Information update done.") except AIOGitHubException: self.system.status.background_task = False self.data.write() self.logger.debug("Recuring background task for all repositories done") return await self.load_known_repositories() self.clear_out_blacklisted_repositories() self.system.status.background_task = False self.data.write() self.hass.bus.fire("hacs/repository", {"action": "reload"}) self.logger.debug("Recuring background task for all repositories done") def clear_out_blacklisted_repositories(self): """Clear out blaclisted repositories.""" need_to_save = False for repository in self.common.blacklist: if self.is_known(repository): repository = self.get_by_name(repository) if repository.status.installed: self.logger.error( f"You have {repository.information.full_name} installed with HACS, this repositroy have not been blacklisted, please consider removing it." ) else: need_to_save = True repository.remove() if need_to_save: self.data.write() async def get_repositories(self): """Return a list of repositories.""" repositories = {} if self.configuration.dev: if self.developer.devcontainer: repositories = { "appdaemon": ["ludeeus/ad-hacs"], "integration": ["ludeeus/integration-hacs"], "plugin": ["maykar/compact-custom-header"], "python_script": ["ludeeus/ps-hacs"], "theme": ["ludeeus/theme-hacs"], } else: for category in self.common.categories: remote = await self.repo.get_contents( f"repositories/{category}", "data" ) repositories[category] = json.loads(remote.content) if category == "plugin": org = await self.github.get_org_repos("custom-cards") for repo in org: repositories[category].append(repo.full_name) if category == "integration": org = await self.github.get_org_repos("custom-components") for repo in org: repositories[category].append(repo.full_name) for category in repositories: for repo in repositories[category]: if repo not in self.common.default: self.common.default.append(repo) return repositories async def load_known_repositories(self): """Load known repositories.""" self.logger.info("Loading known repositories") blacklist = await self.repo.get_contents("repositories/blacklist", "data") repositories = await self.get_repositories() for item in json.loads(blacklist.content): if item not in self.common.blacklist: self.common.blacklist.append(item) for category in repositories: for repo in repositories[category]: if repo in self.common.blacklist: continue if self.is_known(repo): continue try: await self.register_repository(repo, category) except (AIOGitHubException, AIOGitHubRatelimit): pass
def __init__(self, local_path): """initialize.""" self.logger = Logger("hacs.backup") self.local_path = local_path self.backup_path = "/tmp/hacs_backup"
def __init__(self): """Initialize.""" self.logger = Logger("hacs.api") self.url = self.hacsapi + "/{endpoint}"
class Hacs: """The base class of HACS, nested thoughout the project.""" token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}" hacsweb = f"/hacsweb/{token}" hacsapi = f"/hacsapi/{token}" repositories = [] frontend = HacsFrontend() repo = None data_repo = None developer = Developer() data = None configuration = None logger = Logger("hacs") github = None hass = None version = None factory = HacsTaskFactory() system = System() recuring_tasks = [] common = HacsCommon() @staticmethod def init(hass, github_token): """Return a initialized HACS object.""" return Hacs() def get_by_id(self, repository_id): """Get repository by ID.""" try: for repository in self.repositories: if repository.information.uid == repository_id: return repository except Exception: # pylint: disable=broad-except pass return None def get_by_name(self, repository_full_name): """Get repository by full_name.""" try: for repository in self.repositories: if repository.information.full_name == repository_full_name: return repository except Exception: # pylint: disable=broad-except pass return None def is_known(self, repository_full_name): """Return a bool if the repository is known.""" for repository in self.repositories: if repository.information.full_name == repository_full_name: return True return False @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.information.full_name) async def register_repository(self, full_name, category, check=True): """Register a repository.""" from ..repositories.repository import RERPOSITORY_CLASSES if full_name in self.common.skip: if full_name != "hacs/integration": self.logger.debug(f"Skipping {full_name}") return if category not in RERPOSITORY_CLASSES: self.logger.error(f"{category} is not a valid repository category.") return False repository = RERPOSITORY_CLASSES[category](full_name) if check: try: await repository.registration() if self.system.status.new: repository.status.new = False if repository.validate.errors: self.common.skip.append(repository.information.full_name) if not self.system.status.startup: self.logger.error(f"Validation for {full_name} failed.") return repository.validate.errors repository.logger.info("Registration complete") except AIOGitHubException as exception: self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) self.common.skip.append(repository.information.full_name) # if not self.system.status.startup: if self.system.status.startup: self.logger.error( f"Validation for {full_name} failed with {exception}." ) return exception self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "registration", "repository": repository.information.full_name, "repository_id": repository.information.uid, }, ) self.repositories.append(repository) async def startup_tasks(self): """Tasks tha are started after startup.""" self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) await self.handle_critical_repositories_startup() await self.handle_critical_repositories() await self.load_known_repositories() await self.clear_out_blacklisted_repositories() self.recuring_tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_installed, timedelta(minutes=30) ) ) self.recuring_tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_all, timedelta(minutes=800) ) ) self.hass.bus.async_fire("hacs/reload", {"force": True}) await self.recuring_tasks_installed() self.system.status.startup = False self.system.status.new = False self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() 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.logger.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 instored = [] critical = [] was_installed = False try: critical = await self.data_repo.get_contents("critical") critical = json.loads(critical.content) except AIOGitHubException: pass if not critical: self.logger.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: self.common.blacklist.append(repository["repository"]) 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.logger.critical( f"Removing repository {repository['repository']}, it is marked as critical" ) was_installed = True stored["acknowledged"] = False # Uninstall from HACS repo.remove() await repo.uninstall() stored_critical.append(stored) # Save to FS await async_save_to_store(self.hass, "critical", stored_critical) # Resart HASS if was_installed: self.logger.critical("Resarting Home Assistant") self.hass.async_create_task(self.hass.async_stop(100)) async def recuring_tasks_installed(self, notarealarg=None): """Recuring tasks for installed repositories.""" self.logger.debug( "Starting recuring background task for installed repositories" ) self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: if ( repository.status.installed and repository.category in self.common.categories ): self.factory.tasks.append(self.factory.safe_update(repository)) await self.factory.execute() await self.handle_critical_repositories() self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() self.logger.debug("Recuring background task for installed repositories done") async def recuring_tasks_all(self, notarealarg=None): """Recuring tasks for all repositories.""" self.logger.debug("Starting recuring background task for all repositories") self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: if repository.category in self.common.categories: self.factory.tasks.append(self.factory.safe_common_update(repository)) await self.factory.execute() await self.load_known_repositories() await self.clear_out_blacklisted_repositories() self.system.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.logger.debug("Recuring background task for all repositories done") async def clear_out_blacklisted_repositories(self): """Clear out blaclisted repositories.""" need_to_save = False for repository in self.common.blacklist: if self.is_known(repository): repository = self.get_by_name(repository) if repository.status.installed: self.logger.warning( f"You have {repository.information.full_name} installed with HACS " + "this repository has been blacklisted, please consider removing it." ) else: need_to_save = True repository.remove() if need_to_save: await self.data.async_write() async def get_repositories(self): """Return a list of repositories.""" repositories = {} for category in self.common.categories: repositories[category] = await get_default_repos_lists( self.github, category ) org = await get_default_repos_orgs(self.github, category) for repo in org: repositories[category].append(repo) for category in repositories: for repo in repositories[category]: if repo not in self.common.default: self.common.default.append(repo) return repositories async def load_known_repositories(self): """Load known repositories.""" self.logger.info("Loading known repositories") repositories = await self.get_repositories() for item in await get_default_repos_lists(self.github, "blacklist"): if item not in self.common.blacklist: self.common.blacklist.append(item) for category in repositories: for repo in repositories[category]: if repo in self.common.blacklist: continue if self.is_known(repo): continue self.factory.tasks.append( self.factory.safe_register(self, repo, category) ) await self.factory.execute()
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.state = None self.manifest = {} self.repository_manifest = HacsManifest.from_dict({}) self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.logger = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.display_installed_version != self.display_available_version: return True return False @property def ref(self): """Return the ref.""" if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: return self.information.default_branch return "tags/{}".format(self.status.selected_tag) if self.releases.releases: return "tags/{}".format(self.versions.available) return self.information.default_branch @property def custom(self): """Return flag if the repository is custom.""" if self.information.full_name.split("/")[0] in [ "custom-components", "custom-cards", ]: return False if self.information.full_name in self.common.default: return False if self.information.full_name == "hacs/integration": return False return True @property def can_install(self): """Return bool if repository can be installed.""" target = None if self.information.homeassistant_version is not None: target = self.information.homeassistant_version if self.repository_manifest is not None: if self.repository_manifest.homeassistant is not None: target = self.repository_manifest.homeassistant if target is not None: if self.releases.releases: if not version_left_higher_then_right(self.system.ha_version, target): return False return True @property def display_name(self): """Return display name.""" return get_repository_name( self.repository_manifest, self.information.name, self.information.category, self.manifest, ) @property def display_status(self): """Return display_status.""" if self.status.new: status = "new" elif self.pending_restart: status = "pending-restart" elif self.pending_upgrade: status = "pending-upgrade" elif self.status.installed: status = "installed" else: status = "default" return status @property def display_status_description(self): """Return display_status_description.""" description = { "default": "Not installed.", "pending-restart": "Restart pending.", "pending-upgrade": "Upgrade pending.", "installed": "No action required.", "new": "This is a newly added repository.", } return description[self.display_status] @property def display_installed_version(self): """Return display_authors""" if self.versions.installed is not None: installed = self.versions.installed else: if self.versions.installed_commit is not None: installed = self.versions.installed_commit else: installed = "" return installed @property def display_available_version(self): """Return display_authors""" if self.versions.available is not None: available = self.versions.available else: if self.versions.available_commit is not None: available = self.versions.available_commit else: available = "" return available @property def display_version_or_commit(self): """Does the repositoriy use releases or commits?""" if self.releases.releases: version_or_commit = "version" else: version_or_commit = "commit" return version_or_commit @property def main_action(self): """Return the main action.""" actions = { "new": "INSTALL", "default": "INSTALL", "installed": "REINSTALL", "pending-restart": "REINSTALL", "pending-upgrade": "UPGRADE", } return actions[self.display_status] async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Step 6: Get the content of hacs.json await self.get_repository_manifest_content() # Set repository name self.information.name = self.information.full_name.split("/")[1] async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Set description if self.repository_object.description: self.information.description = self.repository_object.description async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Set stargazers_count self.information.stars = self.repository_object.attributes.get( "stargazers_count", 0) # Update default branch self.information.default_branch = self.repository_object.default_branch # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Update last updaeted self.information.last_updated = self.repository_object.pushed_at # Update topics self.information.topics = self.repository_object.topics # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" await self.get_info_md_content() # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" self.validate.errors = [] persistent_directory = None await self.update_repository() if self.repository_manifest: if self.repository_manifest.persistent_directory: if os.path.exists( f"{self.content.path.local}/{self.repository_manifest.persistent_directory}" ): persistent_directory = Backup( f"{self.content.path.local}/{self.repository_manifest.persistent_directory}", tempfile.TemporaryFile() + "/hacs_persistent_directory/", ) persistent_directory.create() if self.status.installed and not self.content.single: backup = Backup(self.content.path.local) backup.create() if self.repository_manifest.zip_release: validate = await self.download_zip(self.validate) else: validate = await self.download_content( self.validate, self.content.path.remote, self.content.path.local, self.ref, ) if validate.errors: for error in validate.errors: self.logger.error(error) if self.status.installed and not self.content.single: backup.restore() if self.status.installed and not self.content.single: backup.cleanup() if persistent_directory is not None: persistent_directory.restore() persistent_directory.cleanup() if validate.success: if self.information.full_name not in self.common.installed: if self.information.full_name == "hacs/integration": self.common.installed.append(self.information.full_name) self.status.installed = True self.versions.installed_commit = self.versions.available_commit if self.status.selected_tag is not None: self.versions.installed = self.status.selected_tag else: self.versions.installed = self.versions.available if self.information.category == "integration": if (self.config_flow and self.information.full_name != "hacs/integration"): await self.reload_custom_components() self.pending_restart = True elif self.information.category == "theme": try: await self.hass.services.async_call( "frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "install", "repository": self.information.full_name, }, ) async def download_zip(self, validate): """Download ZIP archive from repository release.""" try: contents = False for release in self.releases.objects: self.logger.info( f"ref: {self.ref} --- tag: {release.tag_name}") if release.tag_name == self.ref.split("/")[1]: contents = release.assets if not contents: return validate for content in contents or []: filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue result = await async_save_file( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", filecontent, ) with zipfile.ZipFile( f"{tempfile.gettempdir()}/{self.repository_manifest.filename}", "r") as zip_file: zip_file.extractall(self.content.path.local) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception: validate.errors.append(f"Download was not complete.") return validate async def download_content(self, validate, directory_path, local_directory, ref): """Download the content of a directory.""" try: # Get content contents = [] if self.releases.releases: for release in self.releases.objects: if self.status.selected_tag == release.tag_name: contents = release.assets if not contents: if self.content.single: contents = self.content.objects else: contents = await self.repository_object.get_contents( directory_path, self.ref) for content in contents: if content.type == "dir" and ( self.repository_manifest.content_in_root or self.content.path.remote != ""): await self.download_content(validate, content.path, local_directory, ref) continue if self.information.category == "plugin": if not content.name.endswith(".js"): if self.content.path.remote != "dist": continue self.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue # Save the content of the file. if self.content.single: local_directory = self.content.path.local else: _content_path = content.path if not self.repository_manifest.content_in_root: _content_path = _content_path.replace( f"{self.content.path.remote}/", "") local_directory = f"{self.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except Exception: validate.errors.append(f"Download was not complete.") return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest.from_dict( json.loads(manifest.content)) except (AIOGitHubException, Exception): # Gotta Catch 'Em All pass async def get_info_md_content(self): """Get the content of info.md""" from ..handler.template import render_template info = None info_files = ["info", "info.md"] if self.repository_manifest is not None: if self.repository_manifest.render_readme: info_files = ["readme", "readme.md"] try: root = await self.repository_object.get_contents("", self.ref) for file in root: if file.name.lower() in info_files: info = await self.repository_object.get_contents( file.name, self.ref) break if info is None: self.information.additional_info = "" else: info = info.content.replace("<svg", "<disabled").replace( "</svg", "</disabled") info = info.replace( '<a href="http', '<a rel="noreferrer" target="_blank" href="http') self.information.additional_info = render_template(info, self) except (AIOGitHubException, Exception): self.information.additional_info = "" async def get_releases(self): """Get repository releases.""" if self.status.show_beta: self.releases.objects = await self.repository_object.get_releases( prerelease=True, returnlimit=self.configuration.release_limit) else: self.releases.objects = await self.repository_object.get_releases( prerelease=False, returnlimit=self.configuration.release_limit) if not self.releases.objects: return self.releases.releases = True self.releases.published_tags = [] for release in self.releases.objects: self.releases.published_tags.append(release.tag_name) self.releases.last_release_object = self.releases.objects[0] if self.status.selected_tag is not None: if self.status.selected_tag != self.information.default_branch: for release in self.releases.objects: if release.tag_name == self.status.selected_tag: self.releases.last_release_object = release break self.versions.available = self.releases.objects[0].tag_name def remove(self): """Run remove tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Starting removal") if self.information.uid in self.common.installed: self.common.installed.remove(self.information.uid) for repository in self.repositories: if repository.information.uid == self.information.uid: self.repositories.remove(repository) async def uninstall(self): """Run uninstall tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Uninstalling") await self.remove_local_directory() self.status.installed = False if self.information.category == "integration": if self.config_flow: await self.reload_custom_components() else: self.pending_restart = True elif self.information.category == "theme": try: await self.hass.services.async_call("frontend", "reload_themes", {}) except Exception: # pylint: disable=broad-except pass if self.information.full_name in self.common.installed: self.common.installed.remove(self.information.full_name) self.versions.installed = None self.versions.installed_commit = None self.hass.bus.async_fire( "hacs/repository", { "id": 1337, "action": "uninstall", "repository": self.information.full_name, }, ) async def remove_local_directory(self): """Check the local directory.""" import shutil from asyncio import sleep try: if self.information.category == "python_script": local_path = "{}/{}.py".format(self.content.path.local, self.information.name) elif self.information.category == "theme": local_path = "{}/{}.yaml".format(self.content.path.local, self.information.name) else: local_path = self.content.path.local if os.path.exists(local_path): self.logger.debug(f"Removing {local_path}") if self.information.category in ["python_script", "theme"]: os.remove(local_path) else: shutil.rmtree(local_path) while os.path.exists(local_path): await sleep(1) except Exception as exception: self.logger.debug(f"Removing {local_path} failed with {exception}") return
class HacsData(Hacs): """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") def check_corrupted_files(self): """Return True if one (or more) of the files are corrupted.""" for store in STORES: path = f"{self.system.config_path}/.storage/{STORES[store]}" if os.path.exists(path): if os.stat(path).st_size == 0: # File is empty (corrupted) return True return False def read(self, store): """Return data from a store.""" path = f"{self.system.config_path}/.storage/{STORES[store]}" content = None if os.path.exists(path): with open(path, "r", encoding="utf-8") as storefile: content = storefile.read() content = json.loads(content) return content def write(self): """Write content to the store files.""" if self.system.status.background_task: return self.logger.debug("Saving data") # Hacs path = f"{self.system.config_path}/.storage/{STORES['hacs']}" hacs = {"view": self.configuration.frontend_mode} save(self.logger, path, hacs) # Installed path = f"{self.system.config_path}/.storage/{STORES['installed']}" installed = {} for repository_name in self.common.installed: repository = self.get_by_name(repository_name) if repository is None: self.logger.warning( f"Did not save information about {repository_name}") continue installed[repository.information.full_name] = { "version_type": repository.display_version_or_commit, "version_installed": repository.display_installed_version, "version_available": repository.display_available_version, } save(self.logger, path, installed) # Repositories path = f"{self.system.config_path}/.storage/{STORES['repositories']}" content = {} for repository in self.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.information.authors, "topics": repository.information.topics, "category": repository.information.category, "description": repository.information.description, "full_name": repository.information.full_name, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "repository_manifest": repository_manifest, "name": repository.information.name, "new": repository.status.new, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "version_installed": repository.versions.installed, } # Validate installed repositories count_installed = len(installed) + 1 # For HACS it self count_installed_restore = 0 for repository in self.repositories: if repository.status.installed: count_installed_restore += 1 if count_installed < count_installed_restore: self.logger.debug("Save failed!") self.logger.debug( f"Number of installed repositories does not match the number of stored repositories [{count_installed} vs {count_installed_restore}]" ) return save(self.logger, path, content) async def restore(self): """Restore saved data.""" try: hacs = self.read("hacs") installed = self.read("installed") repositrories = self.read("repositories") if self.check_corrupted_files(): # Coruptted installation self.logger.critical( "Restore failed one or more files are corrupted!") return False if hacs is None and installed is None and repositrories is None: # Assume new install return True self.logger.info("Restore started") # Hacs hacs = hacs["data"] self.configuration.frontend_mode = hacs["view"] # Installed installed = installed["data"] for repository in installed: self.common.installed.append(repository) # Repositories repositrories = repositrories["data"] for entry in repositrories: repo = repositrories[entry] if not self.is_known(repo["full_name"]): await self.register_repository(repo["full_name"], repo["category"], False) repository = self.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes if repo.get("authors") is not None: repository.information.authors = repo["authors"] if repo.get("topics", []): repository.information.topics = repo["topics"] if repo.get("description") is not None: repository.information.description = repo["description"] if repo.get("name") is not None: repository.information.name = repo["name"] if repo.get("hide") is not None: repository.status.hide = repo["hide"] if repo.get("installed") is not None: repository.status.installed = repo["installed"] if repository.status.installed: repository.status.first_install = False if repo.get("selected_tag") is not None: repository.status.selected_tag = repo["selected_tag"] if repo.get("repository_manifest") is not None: repository.repository_manifest = HacsManifest( repo["repository_manifest"]) if repo.get("show_beta") is not None: repository.status.show_beta = repo["show_beta"] if repo.get("last_commit") is not None: repository.versions.available_commit = repo["last_commit"] repository.information.uid = entry if repo.get("last_release_tag") is not None: repository.releases.last_release = repo["last_release_tag"] repository.versions.available = repo["last_release_tag"] if repo.get("new") is not None: repository.status.new = repo["new"] if repo["full_name"] == "custom-components/hacs": repository.versions.installed = VERSION repository.status.installed = True if "b" in VERSION: repository.status.show_beta = True elif repo.get("version_installed") is not None: repository.versions.installed = repo["version_installed"] if repo.get("installed_commit") is not None: repository.versions.installed_commit = repo[ "installed_commit"] if repo["full_name"] in self.common.installed: repository.status.installed = True repository.status.new = False frominstalled = installed[repo["full_name"]] if frominstalled["version_type"] == "commit": repository.versions.installed_commit = frominstalled[ "version_installed"] repository.versions.available_commit = frominstalled[ "version_available"] else: repository.versions.installed = frominstalled[ "version_installed"] repository.versions.available = frominstalled[ "version_available"] # Check the restore. count_installed = len(installed) + 1 # For HACS it self count_installed_restore = 0 installed_restore = [] for repository in self.repositories: if repository.status.installed: installed_restore.append(repository.information.full_name) if (repository.information.full_name not in self.common.installed and repository.information.full_name != "custom-components/hacs"): self.logger.warning( f"{repository.information.full_name} is not in common.installed" ) count_installed_restore += 1 if count_installed < count_installed_restore: for repo in installed: installed_restore.remove(repo) self.logger.warning(f"Check {repo}") self.logger.critical("Restore failed!") self.logger.critical( f"Number of installed repositories does not match the number of restored repositories [{count_installed} vs {count_installed_restore}]" ) return False self.logger.info("Restore done") except Exception as exception: self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsPlugin(HacsRepository): """Plugins in HACS.""" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.file_name = None self.data.category = "plugin" self.information.javascript_type = None self.content.path.local = ( f"{self.hacs.system.config_path}/www/community/{full_name.split('/')[-1]}" ) self.logger = Logger( f"hacs.repository.{self.data.category}.{full_name}") async def validate_repository(self): """Validate.""" # Run common validation steps. await self.common_validate() # Custom step 1: Validate content. find_file_name(self) if self.content.path.remote is None: raise HacsException( f"Repostitory 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.system.status.startup: self.logger.error(error) return self.validate.success async def registration(self): """Registration.""" if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() async def update_repository(self): """Update.""" if self.hacs.github.ratelimits.remaining == 0: return # Run common update steps. await self.common_update() # Get plugin objects. find_file_name(self) # Get JS type await self.parse_readme_for_jstype() if self.content.path.remote is None: self.validate.errors.append("Repostitory structure not compliant") if self.content.path.remote == "release": self.content.single = True async def get_package_content(self): """Get package content.""" try: package = await self.repository_object.get_contents("package.json") package = json.loads(package.content) if package: self.data.authors = package["author"] except Exception: # pylint: disable=broad-except pass async def parse_readme_for_jstype(self): """Parse the readme looking for js type.""" readme = None readme_files = ["readme", "readme.md"] root = await self.repository_object.get_contents("") for file in root: if file.name.lower() in readme_files: readme = await self.repository_object.get_contents(file.name) break if readme is None: return readme = readme.content for line in readme.splitlines(): if "type: module" in line: self.information.javascript_type = "module" break elif "type: js" in line: self.information.javascript_type = "js" break
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.repository_manifest = None self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.logger = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.display_installed_version != self.display_available_version: return True return False @property def ref(self): """Return the ref.""" if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: return self.information.default_branch return "tags/{}".format(self.status.selected_tag) if self.releases.releases: return "tags/{}".format(self.versions.available) return self.information.default_branch @property def custom(self): """Return flag if the repository is custom.""" if self.information.full_name.split("/")[0] in [ "custom-components", "custom-cards", ]: return False if self.information.full_name in self.common.default: return False return True @property def can_install(self): """Return bool if repository can be installed.""" target = None if self.information.homeassistant_version is not None: target = self.information.homeassistant_version if self.repository_manifest is not None: if self.repository_manifest.homeassistant is not None: target = self.repository_manifest.homeassistant if target is not None: if self.releases.releases: if LooseVersion(self.system.ha_version) < LooseVersion(target): return False return True @property def display_name(self): """Return display name.""" name = None if self.information.category == "integration": if self.manifest is not None: name = self.manifest["name"] if self.repository_manifest is not None: name = self.repository_manifest.name if name is not None: return name if self.information.name: name = self.information.name.replace("-", " ").replace("_", " ").title() if name is not None: return name name = self.information.full_name return name @property def display_status(self): """Return display_status.""" if self.status.new: status = "new" elif self.pending_restart: status = "pending-restart" elif self.pending_upgrade: status = "pending-upgrade" elif self.status.installed: status = "installed" else: status = "default" return status @property def display_status_description(self): """Return display_status_description.""" description = { "default": "Not installed.", "pending-restart": "Restart pending.", "pending-upgrade": "Upgrade pending.", "installed": "No action required.", "new": "This is a newly added repository.", } return description[self.display_status] @property def display_installed_version(self): """Return display_authors""" if self.versions.installed is not None: installed = self.versions.installed else: if self.versions.installed_commit is not None: installed = self.versions.installed_commit else: installed = "" return installed @property def display_available_version(self): """Return display_authors""" if self.versions.available is not None: available = self.versions.available else: if self.versions.available_commit is not None: available = self.versions.available_commit else: available = "" return available @property def display_version_or_commit(self): """Does the repositoriy use releases or commits?""" if self.versions.installed is not None: version_or_commit = "version" else: version_or_commit = "commit" return version_or_commit @property def main_action(self): """Return the main action.""" actions = { "new": "INSTALL", "default": "INSTALL", "installed": "REINSTALL", "pending-restart": "REINSTALL", "pending-upgrade": "UPGRADE", } return actions[self.display_status] async def common_validate(self): """Common validation steps of the repository.""" # Attach helpers self.validate.errors = [] self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Step 1: Make sure the repository exist. self.logger.debug("Checking repository.") try: self.repository_object = await self.github.get_repo( self.information.full_name) except Exception as exception: # Gotta Catch 'Em All if not self.system.status.startup: self.logger.error(exception) self.validate.errors.append("Repository does not exist.") return # Step 2: Make sure the repository is not archived. if self.repository_object.archived: self.validate.errors.append("Repository is archived.") return # Step 3: Make sure the repository is not in the blacklist. if self.information.full_name in self.common.blacklist: self.validate.errors.append("Repository is in the blacklist.") return # Step 4: default branch self.information.default_branch = self.repository_object.default_branch # Step 5: Get releases. await self.get_releases() # Set repository name self.information.name = self.information.full_name.split("/")[1] async def common_registration(self): """Common registration steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository if self.repository_object is None: self.repository_object = await self.github.get_repo( self.information.full_name) # Set id self.information.uid = str(self.repository_object.id) # Set topics self.information.topics = self.repository_object.topics # Set description if self.repository_object.description: self.information.description = self.repository_object.description async def common_update(self): """Common information update steps of the repository.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update description if self.repository_object.description: self.information.description = self.repository_object.description # Update default branch if self.information.full_name != "custom-components/hacs": self.information.default_branch = self.repository_object.default_branch else: self.information.default_branch = "next" # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Update last updaeted self.information.last_updated = self.repository_object.pushed_at # Update topics self.information.topics = self.repository_object.topics # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" await self.get_info_md_content() # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" self.validate.errors = [] await self.update_repository() if self.status.installed and not self.content.single: backup = Backup(self.content.path.local) backup.create() validate = await self.download_content(self.validate, self.content.path.remote, self.content.path.local, self.ref) if validate.errors: for error in validate.errors: self.logger.error(error) if self.status.installed and not self.content.single: backup.restore() if self.status.installed and not self.content.single: backup.cleanup() if validate.success: if self.information.full_name not in self.common.installed: if self.information.full_name != "custom-components/hacs": self.common.installed.append(self.information.full_name) self.status.installed = True self.versions.installed_commit = self.versions.available_commit if self.status.selected_tag is not None: self.versions.installed = self.status.selected_tag else: self.versions.installed = self.versions.available if self.information.category == "integration": if (self.config_flow and self.information.full_name != "custom-components/hacs"): await self.reload_custom_components() else: self.pending_restart = True async def download_content(self, validate, directory_path, local_directory, ref): """Download the content of a directory.""" try: # Get content if self.content.single: contents = self.content.objects else: contents = await self.repository_object.get_contents( directory_path, self.ref) for content in contents: if content.type == "dir" and self.content.path.remote != "": await self.download_content(validate, content.path, local_directory, ref) continue if self.information.category == "plugin": if not content.name.endswith(".js"): if self.content.path.remote != "dist": continue self.logger.debug(f"Downloading {content.name}") filecontent = await async_download_file( self.hass, content.download_url) if filecontent is None: validate.errors.append( f"[{content.name}] was not downloaded.") continue # Save the content of the file. if self.content.single: local_directory = self.content.path.local else: _content_path = content.path _content_path = _content_path.replace( f"{self.content.path.remote}/", "") local_directory = f"{self.content.path.local}/{_content_path}" local_directory = local_directory.split("/") del local_directory[-1] local_directory = "/".join(local_directory) # Check local directory pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) local_file_path = f"{local_directory}/{content.name}" result = await async_save_file(local_file_path, filecontent) if result: self.logger.info(f"download of {content.name} complete") continue validate.errors.append(f"[{content.name}] was not downloaded.") except SystemError: pass return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" try: manifest = await self.repository_object.get_contents( "hacs.json", self.ref) self.repository_manifest = HacsManifest( json.loads(manifest.content)) except AIOGitHubException: # Gotta Catch 'Em All pass async def get_info_md_content(self): """Get the content of info.md""" from ..handler.template import render_template info = None info_files = ["info", "info.md"] if self.repository_manifest is not None: if self.repository_manifest.render_readme: info_files = ["readme", "readme.md"] try: root = await self.repository_object.get_contents("", self.ref) for file in root: if file.name.lower() in info_files: info = await self.repository_object.get_contents( file.name, self.ref) break if info is None: self.information.additional_info = "" else: info = await self.github.render_markdown(info.content) info = info.replace("<", "<") info = info.replace("<h3>", "<h6>").replace("</h3>", "</h6>") info = info.replace("<h2>", "<h5>").replace("</h2>", "</h5>") info = info.replace("<h1>", "<h4>").replace("</h1>", "</h4>") info = info.replace("<code>", "<code class='codeinfo'>") info = info.replace( '<a href="http', '<a rel="noreferrer" target="_blank" href="http') info = info.replace("<ul>", "") info = info.replace("</ul>", "") info += "</br>" self.information.additional_info = render_template(info, self) except Exception: # Gotta Catch 'Em All self.information.additional_info = "" async def get_releases(self): """Get repository releases.""" if self.status.show_beta: temp = await self.repository_object.get_releases(prerelease=True) else: temp = await self.repository_object.get_releases(prerelease=False) if not temp: return self.releases.releases = True self.releases.published_tags = [] for release in temp: self.releases.published_tags.append(release.tag_name) self.releases.last_release_object = temp[0] if self.status.selected_tag is not None: if self.status.selected_tag != self.information.default_branch: for release in temp: if release.tag_name == self.status.selected_tag: self.releases.last_release_object = release break self.versions.available = temp[0].tag_name def remove(self): """Run remove tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Starting removal") if self.information.uid in self.common.installed: self.common.installed.remove(self.information.uid) for repository in self.repositories: if repository.information.uid == self.information.uid: self.repositories.remove(repository) async def uninstall(self): """Run uninstall tasks.""" # Attach logger if self.logger is None: self.logger = Logger( f"hacs.repository.{self.information.category}.{self.information.full_name}" ) self.logger.info("Uninstalling") await self.remove_local_directory() self.status.installed = False if self.information.category == "integration": if self.config_flow: await self.reload_custom_components() else: self.pending_restart = True if self.information.full_name in self.common.installed: self.common.installed.remove(self.information.full_name) self.versions.installed = None self.versions.installed_commit = None async def remove_local_directory(self): """Check the local directory.""" import os import shutil from asyncio import sleep try: if self.information.category == "python_script": local_path = "{}/{}.py".format(self.content.path.local, self.information.name) elif self.information.category == "theme": local_path = "{}/{}.yaml".format(self.content.path.local, self.information.name) else: local_path = self.content.path.local if os.path.exists(local_path): self.logger.debug(f"Removing {local_path}") if self.information.category in ["python_script", "theme"]: os.remove(local_path) else: shutil.rmtree(local_path) while os.path.exists(local_path): await sleep(1) except Exception as exception: self.logger.debug(f"Removing {local_path} failed with {exception}") return
class Hacs: """The base class of HACS, nested thoughout the project.""" token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}" hacsweb = f"/hacsweb/{token}" hacsapi = f"/hacsapi/{token}" repositories = [] frontend = HacsFrontend() repo = None data_repo = None developer = Developer() data = None configuration = None logger = Logger("hacs") github = None hass = None version = None session = None factory = HacsTaskFactory() queue = QueueManager() system = System() recuring_tasks = [] common = HacsCommon() @staticmethod def init(hass, github_token): """Return a initialized HACS object.""" return Hacs() def get_by_id(self, repository_id): """Get repository by ID.""" try: for repository in self.repositories: if repository.information.uid == repository_id: return repository except Exception: # pylint: disable=broad-except pass return None def get_by_name(self, repository_full_name): """Get repository by full_name.""" try: for repository in self.repositories: if repository.data.full_name.lower() == repository_full_name.lower(): return repository except Exception: # pylint: disable=broad-except pass return None def is_known(self, repository_full_name): """Return a bool if the repository is known.""" return repository_full_name.lower() in [ x.data.full_name.lower() for x in self.repositories ] @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=True) async def startup_tasks(self): """Tasks tha are started after startup.""" self.system.status.background_task = True await self.hass.async_add_executor_job(setup_extra_stores) self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) await self.handle_critical_repositories_startup() await self.handle_critical_repositories() await self.load_known_repositories() await self.clear_out_removed_repositories() self.recuring_tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_installed, timedelta(minutes=30) ) ) self.recuring_tasks.append( async_track_time_interval( self.hass, self.recuring_tasks_all, timedelta(minutes=800) ) ) self.recuring_tasks.append( async_track_time_interval( self.hass, self.prosess_queue, timedelta(minutes=10) ) ) self.hass.bus.async_fire("hacs/reload", {"force": True}) await self.recuring_tasks_installed() await self.prosess_queue() self.system.status.startup = False self.system.status.new = False self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() 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.logger.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 instored = [] critical = [] was_installed = False try: critical = await self.data_repo.get_contents("critical") critical = json.loads(critical.content) except AIOGitHubException: pass if not critical: self.logger.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.logger.critical( f"Removing repository {repository['repository']}, it is marked as critical" ) was_installed = True stored["acknowledged"] = False # Uninstall from HACS repo.remove() await repo.uninstall() stored_critical.append(stored) removed_repo.update_data(stored) # Save to FS await async_save_to_store(self.hass, "critical", stored_critical) # Resart HASS if was_installed: self.logger.critical("Resarting Home Assistant") self.hass.async_create_task(self.hass.async_stop(100)) async def prosess_queue(self, notarealarg=None): """Recuring tasks for installed repositories.""" if not self.queue.has_pending_tasks: self.logger.debug("Nothing in the queue") return if self.queue.running: self.logger.debug("Queue is already running") return can_update = await get_fetch_updates_for(self.github) if can_update == 0: self.logger.info( "HACS is ratelimited, repository updates will resume later." ) else: self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) await self.queue.execute(can_update) self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) async def recuring_tasks_installed(self, notarealarg=None): """Recuring tasks for installed repositories.""" self.logger.debug( "Starting recuring background task for installed repositories" ) self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: if ( repository.status.installed and repository.data.category in self.common.categories ): self.queue.add(self.factory.safe_update(repository)) await self.handle_critical_repositories() self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() self.logger.debug("Recuring background task for installed repositories done") async def recuring_tasks_all(self, notarealarg=None): """Recuring tasks for all repositories.""" self.logger.debug("Starting recuring background task for all repositories") await self.hass.async_add_executor_job(setup_extra_stores) self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) self.logger.debug(self.github.ratelimits.remaining) self.logger.debug(self.github.ratelimits.reset_utc) for repository in self.repositories: if repository.data.category in self.common.categories: self.queue.add(self.factory.safe_common_update(repository)) await self.load_known_repositories() await self.clear_out_removed_repositories() self.system.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.logger.debug("Recuring background task for all repositories done") async def clear_out_removed_repositories(self): """Clear out blaclisted repositories.""" need_to_save = False for removed in removed_repositories: if self.is_known(removed.repository): repository = self.get_by_name(removed.repository) if repository.status.installed and removed.removal_type != "critical": self.logger.warning( f"You have {repository.data.full_name} installed with HACS " + f"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 get_repositories(self): """Return a list of repositories.""" repositories = {} for category in self.common.categories: repositories[category] = await get_default_repos_lists( self.session, self.configuration.token, category ) org = await get_default_repos_orgs(self.github, category) for repo in org: repositories[category].append(repo) for category in repositories: for repo in repositories[category]: if repo not in self.common.default: self.common.default.append(repo) return repositories async def load_known_repositories(self): """Load known repositories.""" self.logger.info("Loading known repositories") repositories = await self.get_repositories() for item in await get_default_repos_lists( self.session, self.configuration.token, "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 repositories: for repo in repositories[category]: if is_removed(repo): continue if self.is_known(repo): continue self.queue.add(self.factory.safe_register(repo, category))
def __init__(self, local_path, backup_path=BACKUP_PATH): """initialize.""" self.logger = Logger("hacs.backup") self.local_path = local_path self.backup_path = backup_path self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
class HacsData(Hacs): """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") async def async_write(self): """Write content to the store files.""" if self.system.status.background_task: return self.logger.debug("Saving data") # Hacs await async_save_to_store(self.hass, "hacs", {"view": self.configuration.frontend_mode}) # Repositories content = {} for repository in self.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.information.authors, "topics": repository.information.topics, "category": repository.information.category, "description": repository.information.description, "full_name": repository.information.full_name, "hide": repository.status.hide, "installed_commit": repository.versions.installed_commit, "installed": repository.status.installed, "last_commit": repository.versions.available_commit, "last_release_tag": repository.versions.available, "repository_manifest": repository_manifest, "name": repository.information.name, "new": repository.status.new, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "version_installed": repository.versions.installed, } await async_save_to_store(self.hass, "repositories", content) self.hass.bus.async_fire("hacs/repository", {}) self.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hass, "hacs") repositories = await async_load_from_store(self.hass, "repositories") try: if not hacs and not repositories: # Assume new install return True self.logger.info("Restore started") # Hacs self.configuration.frontend_mode = hacs.get("view", "Grid") # Repositories repositories = repositories for entry in repositories: repo = repositories[entry] if repo["full_name"] == "custom-components/hacs": # Skip the old repo location continue if not self.is_known(repo["full_name"]): await self.register_repository(repo["full_name"], repo["category"], False) repository = self.get_by_name(repo["full_name"]) if repository is None: self.logger.error(f"Did not find {repo['full_name']}") continue # Restore repository attributes repository.information.uid = entry await self.hass.async_add_executor_job(restore_repository_data, repository, repo) self.logger.info("Restore done") except Exception as exception: # pylint: disable=broad-except self.logger.critical(f"[{exception}] Restore Failed!") return False return True
class HacsIntegration(HacsRepository): """Integrations in HACS.""" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "integration" self.content.path.remote = "custom_components" self.content.path.local = self.localpath self.logger = Logger( f"hacs.repository.{self.data.category}.{full_name}") @property def localpath(self): """Return localpath.""" return f"{self.hacs.system.config_path}/custom_components/{self.data.domain}" async def validate_repository(self): """Validate.""" await self.common_validate() # Custom step 1: Validate content. if self.data.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: raise HacsException( f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" ) self.content.path.remote = f"custom_components/{name}" try: await get_integration_manifest(self) except HacsException as exception: if self.hacs.action: raise HacsException(exception) self.logger.error(exception) # Handle potential errors if self.validate.errors: for error in self.validate.errors: if not self.hacs.system.status.startup: self.logger.error(error) return self.validate.success async def registration(self, ref=None): """Registration.""" if ref is not None: self.ref = ref self.force_branch = True if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() # Set local path self.content.path.local = self.localpath async def update_repository(self): """Update.""" if self.hacs.github.ratelimits.remaining == 0: return await self.common_update() if self.data.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}" try: await get_integration_manifest(self) except HacsException as exception: self.logger.error(exception) # Set local path self.content.path.local = self.localpath 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)
class HacsPythonScript(HacsRepository): """python_scripts in HACS.""" category = "python_script" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "python_script" self.content.path.remote = "python_scripts" self.content.path.local = f"{self.hacs.system.config_path}/python_scripts" self.content.single = True self.logger = Logger( f"hacs.repository.{self.data.category}.{full_name}") async def validate_repository(self): """Validate.""" # Run common validation steps. await self.common_validate() # Custom step 1: Validate content. if self.data.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"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.system.status.startup: self.logger.error(error) return self.validate.success async def registration(self): """Registration.""" if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() # Set name find_file_name(self) async def update_repository(self): # lgtm[py/similar-function] """Update.""" if self.hacs.github.ratelimits.remaining == 0: return # Run common update steps. await self.common_update() # Get python_script objects. if self.data.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"Repository structure for {self.ref.replace('tags/','')} is not compliant" ) # Update name find_file_name(self)
class HacsAppdaemon(HacsRepository): """Appdaemon apps in HACS.""" category = "appdaemon" def __init__(self, full_name): """Initialize.""" super().__init__() self.information.full_name = full_name self.information.category = self.category self.content.path.local = self.localpath self.content.path.remote = "apps" self.logger = Logger(f"hacs.repository.{self.category}.{full_name}") @property def localpath(self): """Return localpath.""" return f"{self.hacs.system.config_path}/appdaemon/apps/{self.information.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 AIOGitHubException: raise HacsException( f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" ) if not isinstance(addir, list): self.validate.errors.append("Repostitory structure not compliant") self.content.path.remote = addir[0].path self.information.name = addir[0].name self.content.objects = await self.repository_object.get_contents( self.content.path.remote, self.ref) self.content.files = [] for filename in self.content.objects: self.content.files.append(filename.name) # Handle potential errors if self.validate.errors: for error in self.validate.errors: if not self.hacs.system.status.startup: self.logger.error(error) return self.validate.success async def registration(self): """Registration.""" if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() # Set local path self.content.path.local = self.localpath async def update_repository(self): """Update.""" if self.hacs.github.ratelimits.remaining == 0: return await self.common_update() # Get appdaemon objects. if self.repository_manifest: if self.data.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.information.name = addir[0].name self.content.objects = await self.repository_object.get_contents( self.content.path.remote, self.ref) self.content.files = [] for filename in self.content.objects: self.content.files.append(filename.name) # Set local path self.content.path.local = self.localpath
def __init__(self): """Initialize.""" self.logger = Logger("hacs.data")
class HacsTheme(HacsRepository): """Themes in HACS.""" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "theme" self.content.path.remote = "themes" self.content.path.local = f"{self.hacs.system.config_path}/themes/" self.content.single = False self.logger = Logger( f"hacs.repository.{self.data.category}.{full_name}") 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"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" ) if self.data.content_in_root: self.content.path.remote = "" # Handle potential errors if self.validate.errors: for error in self.validate.errors: if not self.hacs.system.status.startup: self.logger.error(error) return self.validate.success async def registration(self, ref=None): """Registration.""" if ref is not None: self.ref = ref self.force_branch = True if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() # Set name find_file_name(self) self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" async def update_repository(self, ignore_issues=False): """Update.""" await self.common_update(ignore_issues) # Get theme objects. if self.data.content_in_root: self.content.path.remote = "" # Update name find_file_name(self) self.content.path.local = f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}"