async def async_setup_frontend(): """Configure the HACS frontend elements.""" hacs = get_hacs() hacs.log.info("Setup task %s", HacsSetupTask.FRONTEND) hass = hacs.hass # Register themes hass.http.register_static_path(f"{URL_BASE}/themes", hass.config.path("themes")) # Register frontend if hacs.configuration.frontend_repo_url: getLogger().warning( "Frontend development mode enabled. Do not run in production.") hass.http.register_view(HacsFrontendDev()) else: # hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False) # Custom iconset hass.http.register_static_path(f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")) if "frontend_extra_module_url" not in hass.data: hass.data["frontend_extra_module_url"] = set() hass.data["frontend_extra_module_url"].add("/hacsfiles/iconset.js") # Register www/community for all other files hass.http.register_static_path(URL_BASE, hass.config.path("www/community"), cache_headers=False) hacs.frontend.version_running = FE_VERSION hacs.frontend.version_expected = await hass.async_add_executor_job( get_frontend_version) # Add to sidepanel if "hacs" not in hass.data.get("frontend_panels", {}): hass.components.frontend.async_register_built_in_panel( component_name="custom", sidebar_title=hacs.configuration.sidepanel_title, sidebar_icon=hacs.configuration.sidepanel_icon, frontend_url_path="hacs", config={ "_panel_custom": { "name": "hacs-frontend", "embed_iframe": True, "trust_external": False, "js_url": "/hacsfiles/frontend/entrypoint.js", } }, require_admin=True, )
async def async_download_file(url): """Download files, and return the content.""" hacs = get_hacs() logger = getLogger("async_download_file") if url is None: return if "tags/" in url: url = url.replace("tags/", "") logger.debug(f"Downloading {url}") result = None with async_timeout.timeout(60, loop=hacs.hass.loop): request = await hacs.session.get(url) # Make sure that we got a valid result if request.status == 200: result = await request.read() else: raise HacsException( "Got status code {} when trying to download {}".format( request.status, url)) return result
async def async_serve_category_file(requested_file): hacs = get_hacs() logger = getLogger("web.category") try: if requested_file.startswith("themes/"): servefile = f"{hacs.system.config_path}/{requested_file}" else: servefile = f"{hacs.system.config_path}/www/community/{requested_file}" # Serve .gz if it exist if await async_path_exsist(f"{servefile}.gz"): servefile += ".gz" if await async_path_exsist(servefile): logger.debug(f"Serving {requested_file} from {servefile}") response = web.FileResponse(servefile) response.headers["Cache-Control"] = "no-store, max-age=0" response.headers["Pragma"] = "no-store" return response else: logger.error( f"Tried to serve up '{servefile}' but it does not exist") except (Exception, BaseException) as error: logger.debug( f"there was an issue trying to serve {requested_file} - {error}") return web.Response(status=404)
async def hacs_settings(hass, connection, msg): """Handle get media player cover command.""" hacs = get_hacs() logger = getLogger("api.settings") action = msg["action"] logger.debug(f"WS action '{action}'") if action == "set_fe_grid": hacs.configuration.frontend_mode = "Grid" elif action == "onboarding_done": hacs.configuration.onboarding_done = True elif action == "set_fe_table": hacs.configuration.frontend_mode = "Table" elif action == "set_fe_compact_true": hacs.configuration.frontend_compact = False elif action == "set_fe_compact_false": hacs.configuration.frontend_compact = True elif action == "clear_new": for repo in hacs.repositories: if repo.data.new and repo.data.category in msg.get("categories", []): logger.debug(f"Clearing new flag from '{repo.data.full_name}'") repo.data.new = False else: logger.error(f"WS action '{action}' is not valid") hass.bus.async_fire("hacs/config", {}) await hacs.data.async_write() connection.send_message(websocket_api.result_message(msg["id"], {}))
def __init__(self, repository): """Initialize.""" self.repository = repository self.logger = getLogger("backup") self.backup_path = (tempfile.gettempdir() + "/hacs_persistent_netdaemon/" + repository.data.name)
def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.category = "appdaemon" self.content.path.local = self.localpath self.content.path.remote = "apps" self.logger = getLogger(f"repository.{self.data.category}.{full_name}")
def print(self): """Print the current configuration to the log.""" logger = getLogger("configuration") config = self.to_json() for key in config: if key in ["config", "config_entry", "options", "token"]: continue logger.debug(f"{key}: {config[key]}")
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 = self.localpath self.content.single = True self.logger = getLogger(f"repository.{self.data.category}.{full_name}")
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 = self.localpath self.logger = getLogger(f"repository.{self.data.category}.{full_name}")
def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.full_name_lower = full_name.lower() self.data.category = "integration" self.content.path.remote = "custom_components" self.content.path.local = self.localpath self.logger = getLogger(f"repository.{self.data.category}.{full_name}")
def _clear_storage(): """Clear old files from storage.""" hacs = get_hacs() logger = getLogger("startup.clear_storage") storagefiles = ["hacs"] for s_f in storagefiles: path = f"{hacs.system.config_path}/.storage/{s_f}" if os.path.isfile(path): logger.info(f"Cleaning up old storage file {path}") os.remove(path)
def __init__(self, full_name): """Initialize.""" super().__init__() self.data.full_name = full_name self.data.full_name_lower = full_name.lower() self.data.category = "theme" self.content.path.remote = "themes" self.content.path.local = self.localpath self.content.single = False self.logger = getLogger(f"repository.{self.data.category}.{full_name}")
async def remaining(github): """Helper to calculate the remaining calls to github.""" logger = getLogger("custom_components.hacs.remaining_github_calls") try: ratelimits = await github.get_rate_limit() except (BaseException, Exception) as exception: # pylint: disable=broad-except logger.error(exception) return None if ratelimits.get("remaining") is not None: return int(ratelimits["remaining"]) return 0
class MockRepo(RepositoryMethodInstall, RepositoryMethodPreInstall, RepositoryMethodPostInstall): logger = getLogger() content = MockContent() validate = Validate() can_install = False data = RepositoryData() tree = [] hacs = get_hacs() async def update_repository(self): pass
async def get_file_response(requested_file): """Get file.""" logger = getLogger("web") if requested_file in IGNORE: logger.debug(f"Ignoring request for {requested_file}") return web.Response(status=200) if requested_file.startswith("frontend-"): return await async_serve_frontend() elif requested_file == "iconset.js": return serve_iconset() return await async_serve_category_file(requested_file)
def dummy_repository_base(repository=None): if repository is None: repository = HacsRepository() repository.hacs.hass = HomeAssistant() repository.hacs.hass.data = {"custom_components": []} repository.hacs.system.config_path = tempfile.gettempdir() repository.logger = getLogger("test.test") repository.data.full_name = "test/test" repository.data.domain = "test" repository.data.last_version = "3" repository.data.selected_tag = "3" repository.ref = version_to_install(repository) repository.integration_manifest = {"config_flow": False, "domain": "test"} repository.data.published_tags = ["1", "2", "3"] repository.data.update_data(repository_data) return repository
async def async_save_file(location, content): """Save files.""" logger = getLogger("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, BaseException) as error: # pylint: disable=broad-except msg = f"Could not write data to {location} - {error}" logger.error(msg) return False return os.path.exists(location)
def __init__(self): """Set up HacsRepository.""" self.hacs = get_hacs() self.data = RepositoryData() self.content = RepositoryContent() self.content.path = RepositoryPath() self.information = RepositoryInformation() self.repository_object = None self.status = RepositoryStatus() self.state = None self.force_branch = False self.integration_manifest = {} self.repository_manifest = HacsManifest.from_dict({}) self.validate = Validate() self.releases = RepositoryReleases() self.versions = RepositoryVersions() self.pending_restart = False self.tree = [] self.treefiles = [] self.ref = None self.logger = getLogger()
async def async_get_list_from_default(default: str) -> List: """Get repositories from default list.""" hacs = get_hacs() repositories = [] logger = getLogger("async_get_list_from_default") try: repo = await get_repository( hacs.session, hacs.configuration.token, "hacs/default", ) content = await repo.get_contents(default, repo.default_branch) repositories = json.loads(content.content) except (AIOGitHubAPIException, HacsException) as exception: logger.error(exception) except (Exception, BaseException) as exception: logger.error(exception) logger.debug(f"Got {len(repositories)} elements for {default}") return repositories
# pylint: disable=missing-docstring,invalid-name import asyncio from aiogithubapi import AIOGitHubAPIException from custom_components.hacs.helpers.classes.exceptions import HacsException from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.register_repository import ( register_repository, ) max_concurrent_tasks = asyncio.Semaphore(15) sleeper = 5 logger = getLogger("factory") class HacsTaskFactory: def __init__(self): self.tasks = [] self.running = False async def safe_common_update(self, repository): async with max_concurrent_tasks: try: await repository.common_update() except (AIOGitHubAPIException, HacsException) as exception: logger.error("%s - %s", repository.data.full_name, exception) # Due to GitHub ratelimits we need to sleep a bit await asyncio.sleep(sleeper) async def safe_update(self, repository):
class Hacs(HacsHelpers): """The base class of HACS, nested throughout the project.""" token = f"{str(uuid.uuid4())}-{str(uuid.uuid4())}" action = False hacsweb = f"/hacsweb/{token}" hacsapi = f"/hacsapi/{token}" repositories = [] frontend = HacsFrontend() repo = None data_repo = None data = None configuration = None logger = getLogger() github = None hass = None version = None session = None factory = get_factory() queue = get_queue() system = System() recuring_tasks = [] common = HacsCommon() def get_by_id(self, repository_id): """Get repository by ID.""" try: for repository in self.repositories: if str(repository.data.id) == str(repository_id): return repository except (Exception, BaseException): # pylint: disable=broad-except pass return None def get_by_name(self, repository_full_name): """Get repository by full_name.""" try: repository_full_name_lower = repository_full_name.lower() for repository in self.repositories: if repository.data.full_name_lower == repository_full_name_lower: return repository except (Exception, BaseException): # pylint: disable=broad-except pass return None def is_known(self, repository_id): """Return a bool if the repository is known.""" return str(repository_id) in [ str(x.data.id) for x in self.repositories ] @property def sorted_by_name(self): """Return a sorted(by name) list of repository objects.""" return sorted(self.repositories, key=lambda x: x.display_name) @property def sorted_by_repository_name(self): """Return a sorted(by repository_name) list of repository objects.""" return sorted(self.repositories, key=lambda x: x.data.full_name) async def register_repository(self, full_name, category, check=True): """Register a repository.""" await register_repository(full_name, category, check=True) async def startup_tasks(self): """Tasks that are started after startup.""" self.system.status.background_task = True await async_setup_extra_stores() self.hass.bus.async_fire("hacs/status", {}) await self.handle_critical_repositories_startup() await self.handle_critical_repositories() await self.async_load_default_repositories() await self.clear_out_removed_repositories() self.recuring_tasks.append( self.hass.helpers.event.async_track_time_interval( self.recurring_tasks_installed, timedelta(minutes=30))) self.recuring_tasks.append( self.hass.helpers.event.async_track_time_interval( self.recurring_tasks_all, timedelta(minutes=800))) self.recuring_tasks.append( self.hass.helpers.event.async_track_time_interval( self.prosess_queue, timedelta(minutes=10))) self.hass.bus.async_fire("hacs/reload", {"force": True}) await self.recurring_tasks_installed() await self.prosess_queue() self.system.status.startup = False self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() async def handle_critical_repositories_startup(self): """Handled critical repositories during startup.""" alert = False critical = await async_load_from_store(self.hass, "critical") if not critical: return for repo in critical: if not repo["acknowledged"]: alert = True if alert: self.logger.critical("URGENT!: Check the HACS panel!") self.hass.components.persistent_notification.create( title="URGENT!", message="**Check the HACS panel!**") async def handle_critical_repositories(self): """Handled critical repositories during runtime.""" # Get critical repositories critical_queue = QueueManager() instored = [] critical = [] was_installed = False try: critical = await self.data_repo.get_contents("critical") critical = json.loads(critical.content) except AIOGitHubAPIException: pass if not critical: self.logger.debug("No critical repositories") return stored_critical = await async_load_from_store(self.hass, "critical") for stored in stored_critical or []: instored.append(stored["repository"]) stored_critical = [] for repository in critical: removed_repo = get_removed(repository["repository"]) removed_repo.removal_type = "critical" repo = self.get_by_name(repository["repository"]) stored = { "repository": repository["repository"], "reason": repository["reason"], "link": repository["link"], "acknowledged": True, } if repository["repository"] not in instored: if repo is not None and repo.installed: self.logger.critical( f"Removing repository {repository['repository']}, it is marked as critical" ) was_installed = True stored["acknowledged"] = False # Remove from HACS critical_queue.add(repository.uninstall()) repo.remove() stored_critical.append(stored) removed_repo.update_data(stored) # Uninstall await critical_queue.execute() # Save to FS await async_save_to_store(self.hass, "critical", stored_critical) # Restart HASS if was_installed: self.logger.critical("Resarting Home Assistant") self.hass.async_create_task(self.hass.async_stop(100)) async def prosess_queue(self, _notarealarg=None): """Recurring tasks for installed repositories.""" if not self.queue.has_pending_tasks: self.logger.debug("Nothing in the queue") return if self.queue.running: self.logger.debug("Queue is already running") return can_update = await get_fetch_updates_for(self.github) if can_update == 0: self.logger.info( "HACS is ratelimited, repository updates will resume later.") else: self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) await self.queue.execute(can_update) self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) async def recurring_tasks_installed(self, _notarealarg=None): """Recurring tasks for installed repositories.""" self.logger.debug( "Starting recurring background task for installed repositories") self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) for repository in self.repositories: if (repository.data.installed and repository.data.category in self.common.categories): self.queue.add(self.factory.safe_update(repository)) await self.handle_critical_repositories() self.system.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) await self.data.async_write() self.logger.debug( "Recurring background task for installed repositories done") async def recurring_tasks_all(self, _notarealarg=None): """Recurring tasks for all repositories.""" self.logger.debug( "Starting recurring background task for all repositories") await async_setup_extra_stores() self.system.status.background_task = True self.hass.bus.async_fire("hacs/status", {}) for repository in self.repositories: if repository.data.category in self.common.categories: self.queue.add(self.factory.safe_common_update(repository)) await self.async_load_default_repositories() await self.clear_out_removed_repositories() self.system.status.background_task = False await self.data.async_write() self.hass.bus.async_fire("hacs/status", {}) self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) self.logger.debug( "Recurring background task for all repositories done") async def clear_out_removed_repositories(self): """Clear out blaclisted repositories.""" need_to_save = False for removed in list_removed_repositories(): repository = self.get_by_name(removed.repository) if repository is not None: if repository.data.installed and removed.removal_type != "critical": self.logger.warning( f"You have {repository.data.full_name} installed with HACS " + "this repository has been removed, please consider removing it. " + f"Removal reason ({removed.removal_type})") else: need_to_save = True repository.remove() if need_to_save: await self.data.async_write() async def async_load_default_repositories(self): """Load known repositories.""" self.logger.info("Loading known repositories") for item in await async_get_list_from_default("removed"): removed = get_removed(item["repository"]) removed.reason = item.get("reason") removed.link = item.get("link") removed.removal_type = item.get("removal_type") for category in self.common.categories or []: self.queue.add(self.async_get_category_repositories(category)) await self.queue.execute() async def async_get_category_repositories(self, category): repositories = await async_get_list_from_default(category) for repo in repositories: if is_removed(repo): continue repository = self.get_by_name(repo) if repository is not None: if str(repository.data.id) not in self.common.default: self.common.default.append(str(repository.data.id)) else: continue continue self.queue.add(self.factory.safe_register(repo, category))
GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") CHANGED_FILES = os.getenv("CHANGED_FILES", "") REPOSITORY = os.getenv("REPOSITORY", os.getenv("INPUT_REPOSITORY")) CATEGORY = os.getenv("CATEGORY", os.getenv("INPUT_CATEGORY", "")) CATEGORIES = [ "appdaemon", "integration", "netdaemon", "plugin", "python_script", "theme", ] logger = getLogger("action") def error(error: str): logger.error(error) exit() def get_event_data(): if GITHUB_EVENT_PATH is None: return {} with open(GITHUB_EVENT_PATH) as ev: return json.loads(ev.read()) def chose_repository(category):
def __init__(self): """Initialize.""" self.logger = getLogger("data") self.hacs = get_hacs() self.queue = QueueManager() self.content = {}
GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") CHANGED_FILES = os.getenv("CHANGED_FILES", "") REPOSITORY = os.getenv("REPOSITORY", os.getenv("INPUT_REPOSITORY")) CATEGORY = os.getenv("CATEGORY", os.getenv("INPUT_CATEGORY", "")) CATEGORIES = [ "appdaemon", "integration", "netdaemon", "plugin", "python_script", "theme", ] logger = getLogger() def error(error: str): logger.error(error) exit(1) def get_event_data(): if GITHUB_EVENT_PATH is None or not os.path.exists(GITHUB_EVENT_PATH): return {} with open(GITHUB_EVENT_PATH) as ev: return json.loads(ev.read()) def chose_repository(category):
from aiohttp import web from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.helpers.functions.path_exsist import async_path_exsist from custom_components.hacs.share import get_hacs _LOGGER = getLogger() async def async_serve_category_file(request, requested_file): hacs = get_hacs() try: if requested_file.startswith("themes/"): servefile = f"{hacs.core.config_path}/{requested_file}" else: servefile = f"{hacs.core.config_path}/www/community/{requested_file}" if await async_path_exsist(servefile): _LOGGER.debug("Serving %s from %s", requested_file, servefile) response = web.FileResponse(servefile) if requested_file.startswith("themes/"): response.headers["Cache-Control"] = "public, max-age=2678400" else: response.headers["Cache-Control"] = "no-store, max-age=0" response.headers["Pragma"] = "no-store" return response else: _LOGGER.error( "%s tried to request '%s' but the file does not exist", request.remote, servefile,
async def hacs_repository(hass, connection, msg): """Handle get media player cover command.""" hacs = get_hacs() logger = getLogger() data = {} repository = None repo_id = msg.get("repository") action = msg.get("action") if repo_id is None or action is None: return try: repository = hacs.get_by_id(repo_id) logger.debug(f"Running {action} for {repository.data.full_name}") if action == "update": await repository.update_repository(True) repository.status.updated_info = True elif action == "install": repository.data.new = False was_installed = repository.data.installed await repository.async_install() if not was_installed: hass.bus.async_fire("hacs/reload", {"force": True}) elif action == "not_new": repository.data.new = False elif action == "uninstall": repository.data.new = False await repository.update_repository(True) await repository.uninstall() elif action == "hide": repository.data.hide = True elif action == "unhide": repository.data.hide = False elif action == "show_beta": repository.data.show_beta = True await repository.update_repository() elif action == "hide_beta": repository.data.show_beta = False await repository.update_repository() elif action == "toggle_beta": repository.data.show_beta = not repository.data.show_beta await repository.update_repository() elif action == "delete": repository.data.show_beta = False repository.remove() elif action == "release_notes": data = [ { "name": x.attributes["name"], "body": x.attributes["body"], "tag": x.attributes["tag_name"], } for x in repository.releases.objects ] elif action == "set_version": if msg["version"] == repository.data.default_branch: repository.data.selected_tag = None else: repository.data.selected_tag = msg["version"] await repository.update_repository() hass.bus.async_fire("hacs/reload", {"force": True}) else: logger.error(f"WS action '{action}' is not valid") await hacs.data.async_write() message = None except AIOGitHubAPIException as exception: message = exception except AttributeError as exception: message = f"Could not use repository with ID {repo_id} ({exception})" except (Exception, BaseException) as exception: # pylint: disable=broad-except message = exception if message is not None: logger.error(message) hass.bus.async_fire("hacs/error", {"message": str(message)}) if repository: repository.state = None connection.send_message(websocket_api.result_message(msg["id"], data))
def __init__(self, local_path, backup_path=BACKUP_PATH): """initialize.""" self.logger = getLogger("backup") self.local_path = local_path self.backup_path = backup_path self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}"
def test_logger(): os.environ["GITHUB_ACTION"] = "value" getLogger() del os.environ["GITHUB_ACTION"]
async def hacs_repository_data(hass, connection, msg): """Handle get media player cover command.""" hacs = get_hacs() logger = getLogger("api.repository_data") repo_id = msg.get("repository") action = msg.get("action") data = msg.get("data") if repo_id is None: return if action == "add": if "github." in repo_id: repo_id = repo_id.split("github.com/")[1] if repo_id in hacs.common.skip: hacs.common.skip.remove(repo_id) if not hacs.get_by_name(repo_id): try: registration = await register_repository(repo_id, data.lower()) if registration is not None: raise HacsException(registration) except ( Exception, BaseException, ) as exception: # pylint: disable=broad-except hass.bus.async_fire( "hacs/error", { "action": "add_repository", "exception": str(sys.exc_info()[0].__name__), "message": str(exception), }, ) else: hass.bus.async_fire( "hacs/error", { "action": "add_repository", "message": f"Repository '{repo_id}' exists in the store.", }, ) repository = hacs.get_by_name(repo_id) else: repository = hacs.get_by_id(repo_id) if repository is None: hass.bus.async_fire("hacs/repository", {}) return logger.debug(f"Running {action} for {repository.data.full_name}") try: if action == "set_state": repository.state = data elif action == "set_version": repository.data.selected_tag = data await repository.update_repository() repository.state = None elif action == "install": was_installed = repository.data.installed repository.data.selected_tag = data await repository.update_repository() await repository.async_install() repository.state = None if not was_installed: hass.bus.async_fire("hacs/reload", {"force": True}) elif action == "add": repository.state = None else: repository.state = None logger.error(f"WS action '{action}' is not valid") message = None except AIOGitHubAPIException as exception: message = exception except AttributeError as exception: message = f"Could not use repository with ID {repo_id} ({exception})" except (Exception, BaseException) as exception: # pylint: disable=broad-except message = exception if message is not None: logger.error(message) hass.bus.async_fire("hacs/error", {"message": str(message)}) await hacs.data.async_write() connection.send_message(websocket_api.result_message(msg["id"], {}))
from homeassistant import config_entries from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from custom_components.hacs.const import DOMAIN from custom_components.hacs.helpers.functions.configuration_schema import ( hacs_base_config_schema, hacs_config_option_schema, ) from custom_components.hacs.helpers.functions.information import get_repository # pylint: disable=dangerous-default-value from custom_components.hacs.helpers.functions.logger import getLogger from custom_components.hacs.share import get_hacs _LOGGER = getLogger(__name__) class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for HACS.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize.""" self._errors = {} async def async_step_user(self, user_input={}): """Handle a flow initialized by the user.""" self._errors = {}