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) 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)
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 get_default_repos_lists(session, token, default: str) -> dict: """Gets repositories from default list.""" repositories = [] logger = Logger("hacs") try: repo = await get_repository(session, token, "hacs/default") content = await repo.get_contents(default) repositories = json.loads(content.content) except AIOGitHubAPIException as exception: logger.error(exception) return repositories
async def get_default_repos_lists(github: type(AIOGitHub), default: str) -> dict: """Gets repositories from default list.""" repositories = [] logger = Logger("hacs") try: repo = await github.get_repo("hacs/default") content = await repo.get_contents(default) repositories = json.loads(content.content) except AIOGitHubException as exception: logger.error(exception) return repositories
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 get_default_repos_orgs(github: type(GitHub), 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: return repositories try: repos = await github.get_org_repos(orgs[category]) for repo in repos: repositories.append(repo.full_name) except AIOGitHubAPIException as exception: logger.error(exception) return repositories
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 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 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 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', '')}"
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, 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() async def update_repository(self, ignore_issues=False): """Update.""" await self.common_update(ignore_issues) # Get plugin objects. find_file_name(self) 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
class HacsRepository(Hacs): """HacsRepository.""" def __init__(self): """Set up HacsRepository.""" self.data = {} 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 self.tree = [] self.treefiles = [] self.ref = None @property def pending_upgrade(self): """Return pending upgrade.""" if self.status.installed: if self.status.selected_tag is not None: if self.status.selected_tag == self.information.default_branch: if self.versions.installed_commit != self.versions.available_commit: return True return False if self.display_installed_version != self.display_available_version: return True return False @property def config_flow(self): """Return bool if integration has config_flow.""" if self.manifest: if self.information.full_name == "hacs/integration": return False return self.manifest.get("config_flow", False) return False @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}" ) if self.ref is None: self.ref = version_to_install(self) # 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) self.data = self.repository_object.attributes 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 if not self.tree: self.tree = await self.repository_object.get_tree(self.ref) self.treefiles = [] for treefile in self.tree: self.treefiles.append(treefile.full_path) # 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}" ) self.logger.debug("Getting repository information") # Set ref self.ref = version_to_install(self) # Attach repository self.repository_object = await self.github.get_repo( self.information.full_name) # Update tree self.tree = await self.repository_object.get_tree(self.ref) self.treefiles = [] for treefile in self.tree: self.treefiles.append(treefile.full_path) # 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 updaeted self.information.last_updated = self.repository_object.attributes.get( "pushed_at", 0) # Update topics self.information.topics = self.repository_object.topics # Update last available commit await self.repository_object.set_last_commit() self.versions.available_commit = self.repository_object.last_commit # Get the content of hacs.json await self.get_repository_manifest_content() # Update "info.md" self.information.additional_info = await get_info_md_content(self) # Update releases await self.get_releases() async def install(self): """Common installation steps of the repository.""" await install_repository(self) 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.""" from custom_components.hacs.helpers.download import download_content validate = await download_content(self, validate, local_directory) return validate async def get_repository_manifest_content(self): """Get the content of the hacs.json file.""" if self.ref is None: self.ref = version_to_install(self) 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_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 if self.releases.last_release_object.assets: self.releases.last_release_object_downloads = self.releases.last_release_object.assets[ 0].attributes.get("download_count") 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 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 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 = self.localpath self.content.single = False self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") @property def localpath(self): """Return localpath.""" return f"{self.hacs.system.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" async def async_post_installation(self): """Run post installation steps.""" try: await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) self.logger.info("Themes reloaded") except Exception: # pylint: disable=broad-except pass 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 async_post_registration(self): """Registration.""" # Set name find_file_name(self) self.content.path.local = self.localpath 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 = self.localpath
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 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 HacsNetdaemon(HacsRepository): """Netdaemon apps in HACS.""" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "netdaemon" self.content.path.local = self.localpath self.content.path.remote = "apps" self.logger = Logger(f"hacs.repository.{self.data.category}.{full_name}") @property def localpath(self): """Return localpath.""" return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}" async def validate_repository(self): """Validate.""" await self.common_validate() # Custom step 1: Validate content. if self.repository_manifest: if self.data.content_in_root: self.content.path.remote = "" if self.content.path.remote == "apps": self.data.domain = get_first_directory_in_directory( self.tree, self.content.path.remote ) self.content.path.remote = f"apps/{self.data.name}" compliant = False for treefile in self.treefiles: if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith( ".cs" ): compliant = True break if not compliant: raise HacsException( f"Repostitory 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 update_repository(self, ignore_issues=False): """Update.""" await self.common_update(ignore_issues) # Get appdaemon objects. if self.repository_manifest: if self.data.content_in_root: self.content.path.remote = "" if self.content.path.remote == "apps": self.data.domain = get_first_directory_in_directory( self.tree, self.content.path.remote ) self.content.path.remote = f"apps/{self.data.name}" # Set local path self.content.path.local = self.localpath async def async_post_installation(self): """Run post installation steps.""" try: await self.hacs.hass.services.async_call( "hassio", "addon_restart", {"addon": "c6a2317c_netdaemon"} ) except Exception: # pylint: disable=broad-except pass
class HacsPlugin(HacsRepository): """Plugins in HACS.""" category = "plugin" def __init__(self, full_name): """Initialize.""" super().__init__() self.information.full_name = full_name self.information.category = self.category self.information.file_name = None 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.category}.{full_name}") async def validate_repository(self): """Validate.""" # Run common validation steps. await self.common_validate() # Custom step 1: Validate content. await self.get_plugin_location() 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 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() 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. await self.get_plugin_location() # 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 self.content.files = [] for filename in self.content.objects: self.content.files.append(filename.name) async def get_plugin_location(self): """Get plugin location.""" if self.content.path.remote is not None: return possible_locations = ["dist", "release", ""] if self.data.content_in_root: possible_locations = [""] for location in possible_locations: if self.content.path.remote is not None: continue try: objects = [] files = [] if location != "release": try: objects = await self.repository_object.get_contents( location, self.ref ) except AIOGitHubException: continue else: if self.releases.releases: if self.releases.last_release_object.assets is not None: objects = self.releases.last_release_object.assets for item in objects: if item.name.endswith(".js"): files.append(item.name) # Handler for plug requirement 3 valid_filenames = [ f"{self.information.name.replace('lovelace-', '')}.js", f"{self.information.name}.js", f"{self.information.name}.umd.js", f"{self.information.name}-bundle.js", ] if self.repository_manifest: if self.data.filename: valid_filenames.append(self.data.filename) for name in valid_filenames: if name in files: # YES! We got it! self.information.file_name = name self.content.path.remote = location self.content.objects = objects self.content.files = files break except SystemError: pass 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.information.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 HacsData: """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") self.hacs = get_hacs() async def async_write(self): """Write content to the store files.""" if self.hacs.system.status.background_task or self.hacs.system.disabled: return self.logger.debug("Saving data") # Hacs await async_save_to_store( self.hacs.hass, "hacs", { "view": self.hacs.configuration.frontend_mode, "compact": self.hacs.configuration.frontend_compact, "onboarding_done": self.hacs.configuration.onboarding_done, }, ) # Repositories content = {} for repository in self.hacs.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None data = { "authors": repository.data.authors, "category": repository.data.category, "description": repository.data.description, "domain": repository.data.domain, "downloads": repository.data.downloads, "full_name": repository.data.full_name, "first_install": repository.status.first_install, "installed_commit": repository.data.installed_commit, "installed": repository.data.installed, "last_commit": repository.data.last_commit, "last_release_tag": repository.data.last_version, "last_updated": repository.data.last_updated, "name": repository.data.name, "new": repository.data.new, "repository_manifest": repository_manifest, "selected_tag": repository.data.selected_tag, "show_beta": repository.data.show_beta, "stars": repository.data.stargazers_count, "topics": repository.data.topics, "version_installed": repository.data.installed_version, } if data: if repository.data.installed and ( repository.data.installed_commit or repository.data.installed_version): await async_save_to_store( self.hacs.hass, f"hacs/{repository.data.id}.hacs", repository.data.to_json(), ) content[str(repository.data.id)] = data await async_save_to_store(self.hacs.hass, "repositories", content) self.hacs.hass.bus.async_fire("hacs/repository", {}) self.hacs.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hacs.hass, "hacs") repositories = await async_load_from_store(self.hacs.hass, "repositories") try: if not hacs and not repositories: # Assume new install self.hacs.system.status.new = True return True self.logger.info("Restore started") self.hacs.system.status.new = False # Hacs self.hacs.configuration.frontend_mode = hacs.get("view", "Grid") self.hacs.configuration.frontend_compact = hacs.get( "compact", False) self.hacs.configuration.onboarding_done = hacs.get( "onboarding_done", False) # Repositories for entry in repositories: repo = repositories[entry] if not self.hacs.is_known(entry): await register_repository(repo["full_name"], repo["category"], False) repository = [ x for x in self.hacs.repositories if str(x.data.id) == str(entry) or x.data.full_name == repo["full_name"] ] if not repository: self.logger.error( f"Did not find {repo['full_name']} ({entry})") continue repository = repository[0] # Restore repository attributes repository.data.id = entry await self.hacs.hass.async_add_executor_job( restore_repository_data, repository, repo) restored = await async_load_from_store(self.hacs.hass, f"hacs/{entry}.hacs") if restored: repository.data.update_data(restored) if not repository.data.installed: repository.logger.debug( "Should be installed but is not... Fixing that!") repository.data.installed = True 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 HacsPythonScript(HacsRepository): """python_scripts in HACS.""" category = "python_script" def __init__(self, full_name): """Initialize.""" super().__init__() self.information.full_name = full_name self.information.category = self.category 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.category}.{full_name}") async def validate_repository(self): """Validate.""" # Run common validation steps. await self.common_validate() # Custom step 1: Validate content. try: self.content.objects = await self.repository_object.get_contents( self.content.path.remote, self.ref) except AIOGitHubException: raise HacsException( f"Repostitory structure for {self.ref.replace('tags/','')} is not compliant" ) if not isinstance(self.content.objects, list): self.validate.errors.append("Repostitory structure not compliant") 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 name self.information.name = self.content.objects[0].name.replace(".py", "") 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.repository_manifest: if self.data.content_in_root: self.content.path.remote = "" 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) # Update name self.information.name = self.content.objects[0].name.replace(".py", "") self.content.files = [] for filename in self.content.objects: self.content.files.append(filename.name)
class WebClient: """Web client.""" def __init__(self, session=None, logger=None): """ Initialize. Sample Usage: from integrationhelper.webclient import WebClient url = "https://sample.com/api" webclient = WebClient() print(await webclient.async_get_json(url)) """ self.session = session if logger is not None: self.logger = logger else: from integrationhelper import Logger self.logger = Logger(__name__) @backoff.on_exception(backoff.expo, Exception, max_tries=3) async def async_get_json(self, url: str, custom_headers: dict = None): """Get json response from server.""" try: assert isinstance(url, str) except AssertionError: self.logger.error(f"({url}) is not a string.") return None try: assert custom_headers is None or isinstance(custom_headers, dict) except AssertionError: self.logger.error(f"({custom_headers}) is not a dict.") return None headers = {"Content-Type": "application/json"} if custom_headers is not None: for header in custom_headers: headers[header] = custom_headers[header] jsondata = None try: if self.session is not None: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await self.session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return jsondata jsondata = await response.json() else: async with aiohttp.ClientSession() as session: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return jsondata jsondata = await response.json() self.logger.debug(jsondata) except asyncio.TimeoutError as error: self.logger.error( f"Timeout error fetching information from {url} - ({error})") except (KeyError, TypeError) as error: self.logger.error( f"Error parsing information from {url} - ({error})") except (aiohttp.ClientError, socket.gaierror) as error: self.logger.error( f"Error fetching information from {url} - ({error})") except Exception as error: # pylint: disable=broad-except self.logger.error(f"Something really wrong happend! - ({error})") return jsondata @backoff.on_exception(backoff.expo, Exception, max_tries=3) async def async_get_text(self, url: str, custom_headers: dict = None): """Get text response from server.""" try: assert isinstance(url, str) except AssertionError: self.logger.error(f"({url}) is not a string.") return None try: assert url is None or isinstance(custom_headers, dict) except AssertionError: self.logger.error(f"({custom_headers}) is not a dict.") return None headers = {"Content-Type": "application/json"} if custom_headers is not None: for header in custom_headers: headers[header] = custom_headers[header] textdata = None try: if self.session is not None: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await self.session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return textdata textdata = await response.text() else: async with aiohttp.ClientSession() as session: async with async_timeout.timeout( 10, loop=asyncio.get_event_loop()): response = await session.get(url, headers=headers) if response.status not in GOOD_HTTP_CODES: self.logger.error( f"Recieved HTTP code ({response.status}) from {url}" ) return textdata textdata = await response.text() self.logger.debug(textdata) except asyncio.TimeoutError as error: self.logger.error( f"Timeout error fetching information from {url} - ({error})") except (KeyError, TypeError) as error: self.logger.error( f"Error parsing information from {url} - ({error})") except (aiohttp.ClientError, socket.gaierror) as error: self.logger.error( f"Error fetching information from {url} - ({error})") except Exception as error: # pylint: disable=broad-except self.logger.error(f"Something really wrong happend! - ({error})") return textdata
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
class HacsNetdaemon(HacsRepository): """Netdaemon apps in HACS.""" def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "netdaemon" self.content.path.local = self.localpath self.content.path.remote = "apps" self.logger = Logger( f"hacs.repository.{self.data.category}.{full_name}") @property def localpath(self): """Return localpath.""" return f"{self.hacs.system.config_path}/netdaemon/apps/{self.data.name}" async def validate_repository(self): """Validate.""" await self.common_validate() # Custom step 1: Validate content. if self.repository_manifest: if self.data.content_in_root: self.content.path.remote = "" if self.content.path.remote == "apps": self.data.domain = get_first_directory_in_directory( self.tree, self.content.path.remote) self.content.path.remote = f"apps/{self.data.name}" compliant = False for treefile in self.treefiles: if treefile.startswith(f"{self.content.path.remote}" ) and treefile.endswith(".cs"): compliant = True break if not compliant: raise HacsException( f"Repostitory 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 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": self.data.domain = get_first_directory_in_directory( self.tree, self.content.path.remote) self.content.path.remote = f"apps/{self.data.name}" # Set local path self.content.path.local = self.localpath
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 HacsTheme(HacsRepository): """Themes in HACS.""" category = "theme" def __init__(self, full_name): """Initialize.""" super().__init__() self.information.full_name = full_name self.information.category = self.category 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.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): """Registration.""" if not await self.validate_repository(): return False # Run common registration steps. await self.common_registration() # Set name if self.data.filename is not None: self.information.file_name = self.data.filename else: self.information.file_name = find_first_of_filetype( self.content.files, "yaml").split("/")[-1] self.information.name = self.information.file_name.replace(".yaml", "") self.content.path.local = ( f"{self.hacs.system.config_path}/themes/{self.information.name}") 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 theme objects. if self.data.content_in_root: self.content.path.remote = "" # Update name if self.data.filename is not None: self.information.file_name = self.data.filename else: self.information.file_name = find_first_of_filetype( self.content.files, "yaml").split("/")[-1] self.information.name = self.information.file_name.replace(".yaml", "") self.content.path.local = ( f"{self.hacs.system.config_path}/themes/{self.information.name}")
class HacsData: """HacsData class.""" def __init__(self): """Initialize.""" self.logger = Logger("hacs.data") self.hacs = get_hacs() async def async_write(self): """Write content to the store files.""" if self.hacs.system.status.background_task or self.hacs.system.disabled: return self.logger.debug("Saving data") # Hacs await async_save_to_store( self.hacs.hass, "hacs", { "view": self.hacs.configuration.frontend_mode, "compact": self.hacs.configuration.frontend_compact, "onboarding_done": self.hacs.configuration.onboarding_done, }, ) await async_save_to_store(self.hacs.hass, "removed", [x.__dict__ for x in removed_repositories]) # Repositories content = {} for repository in self.hacs.repositories: if repository.repository_manifest is not None: repository_manifest = repository.repository_manifest.manifest else: repository_manifest = None content[repository.information.uid] = { "authors": repository.data.authors, "category": repository.data.category, "description": repository.data.description, "downloads": repository.releases.downloads, "full_name": repository.data.full_name, "first_install": repository.status.first_install, "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, "last_updated": repository.information.last_updated, "name": repository.data.name, "new": repository.status.new, "repository_manifest": repository_manifest, "selected_tag": repository.status.selected_tag, "show_beta": repository.status.show_beta, "stars": repository.data.stargazers_count, "topics": repository.data.topics, "version_installed": repository.versions.installed, } await async_save_to_store(self.hacs.hass, "repositories", content) self.hacs.hass.bus.async_fire("hacs/repository", {}) self.hacs.hass.bus.fire("hacs/config", {}) async def restore(self): """Restore saved data.""" hacs = await async_load_from_store(self.hacs.hass, "hacs") repositories = await async_load_from_store(self.hacs.hass, "repositories") removed = await async_load_from_store(self.hacs.hass, "removed") try: if not hacs and not repositories: # Assume new install self.hacs.system.status.new = True return True self.logger.info("Restore started") # Hacs self.hacs.configuration.frontend_mode = hacs.get("view", "Grid") self.hacs.configuration.frontend_compact = hacs.get( "compact", False) self.hacs.configuration.onboarding_done = hacs.get( "onboarding_done", False) for entry in removed: removed_repo = get_removed(entry["repository"]) removed_repo.update_data(entry) # Repositories for entry in repositories: repo = repositories[entry] if not self.hacs.is_known(repo["full_name"]): await register_repository(repo["full_name"], repo["category"], False) repository = self.hacs.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.hacs.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 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 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 = {} repositories = {} try: hacs = await async_load_from_store(self.hass, "hacs") except KeyError: await async_save_to_store(self.hass, "hacs", self.data.read("hacs")["data"]) hacs = await async_load_from_store(self.hass, "hacs") try: repositories = await async_load_from_store(self.hass, "repositories") except KeyError: await async_save_to_store(self.hass, "repositories", self.data.read("repositories")["data"]) repositories = await async_load_from_store(self.hass, "repositories") try: 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 repositories is None: # 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 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.from_dict( 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"] == "hacs/integration": 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"] self.logger.info("Restore done") except Exception as exception: self.logger.critical( f"[{exception}] Restore Failed! see https://github.com/hacs/integration/issues/639 for more details." ) return False return True