Showing preview only (2,559K chars total). Download the full file or copy to clipboard to get everything.
Repository: hacs/integration
Branch: main
Commit: cb488796faf1
Files: 701
Total size: 2.3 MB
Directory structure:
gitextract_ec9nfn8q/
├── .codeclimate.yml
├── .codecov.yml
├── .coveragerc
├── .devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── a_integration.yml
│ │ ├── b_frontend.yml
│ │ ├── c_bot.yml
│ │ ├── config.yml
│ │ ├── d_documentation.yml
│ │ ├── e_action.yml
│ │ ├── f_addon.yml
│ │ └── removal.yml
│ ├── dependabot.yml
│ ├── pre-commit-config.yaml
│ ├── release.yml
│ └── workflows/
│ ├── action-container.yml
│ ├── generate-hacs-data.yml
│ ├── lint.yaml
│ ├── lock.yml
│ ├── publish.yml
│ ├── pull_requests_labels.yml
│ ├── pytest.yml
│ ├── stale.yml
│ └── validate.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── README.md
├── action/
│ ├── Dockerfile
│ └── action.py
├── constraints.txt
├── custom_components/
│ └── hacs/
│ ├── __init__.py
│ ├── base.py
│ ├── config_flow.py
│ ├── const.py
│ ├── coordinator.py
│ ├── data_client.py
│ ├── diagnostics.py
│ ├── entity.py
│ ├── enums.py
│ ├── exceptions.py
│ ├── frontend.py
│ ├── icons.json
│ ├── iconset.js
│ ├── manifest.json
│ ├── repairs.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── appdaemon.py
│ │ ├── base.py
│ │ ├── integration.py
│ │ ├── plugin.py
│ │ ├── python_script.py
│ │ ├── template.py
│ │ └── theme.py
│ ├── switch.py
│ ├── system_health.py
│ ├── types.py
│ ├── update.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── backup.py
│ │ ├── configuration_schema.py
│ │ ├── data.py
│ │ ├── decode.py
│ │ ├── decorator.py
│ │ ├── file_system.py
│ │ ├── filters.py
│ │ ├── github_graphql_query.py
│ │ ├── json.py
│ │ ├── logger.py
│ │ ├── path.py
│ │ ├── queue_manager.py
│ │ ├── regex.py
│ │ ├── store.py
│ │ ├── url.py
│ │ ├── validate.py
│ │ ├── version.py
│ │ └── workarounds.py
│ ├── validate/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── archived.py
│ │ ├── base.py
│ │ ├── brands.py
│ │ ├── description.py
│ │ ├── hacsjson.py
│ │ ├── images.py
│ │ ├── information.py
│ │ ├── integration_manifest.py
│ │ ├── issues.py
│ │ ├── manager.py
│ │ └── topics.py
│ └── websocket/
│ ├── __init__.py
│ ├── critical.py
│ ├── repositories.py
│ └── repository.py
├── hacs.json
├── info.md
├── pyproject.toml
├── requirements_action.txt
├── requirements_base.txt
├── requirements_core_min.txt
├── requirements_generate_data.txt
├── requirements_lint.txt
├── requirements_test.txt
├── scripts/
│ ├── __init__.py
│ ├── clear_storage
│ ├── coverage
│ ├── data/
│ │ ├── __init__.py
│ │ ├── common.py
│ │ ├── generate_category_data.py
│ │ └── validate_category_data.py
│ ├── develop
│ ├── install/
│ │ ├── core
│ │ ├── core_dev
│ │ ├── frontend
│ │ ├── pip_packages
│ │ └── uv_packages
│ ├── lgtm.js
│ ├── lint
│ ├── setup
│ ├── snapshot-update
│ ├── test
│ └── update/
│ ├── __init__.py
│ ├── default_repositories.py
│ └── manifest.py
└── tests/
├── __init__.py
├── action/
│ └── test_hacs_action_integration.py
├── common.py
├── conftest.py
├── fixtures/
│ ├── proxy/
│ │ ├── api.github.com/
│ │ │ ├── rate_limit.json
│ │ │ └── repos/
│ │ │ ├── hacs/
│ │ │ │ ├── default/
│ │ │ │ │ └── contents/
│ │ │ │ │ ├── appdaemon.json
│ │ │ │ │ ├── integration.json
│ │ │ │ │ ├── plugin.json
│ │ │ │ │ ├── python_script.json
│ │ │ │ │ ├── template.json
│ │ │ │ │ └── theme.json
│ │ │ │ ├── integration/
│ │ │ │ │ ├── contents/
│ │ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ │ └── hacs/
│ │ │ │ │ │ │ └── manifest.json
│ │ │ │ │ │ └── hacs.json
│ │ │ │ │ └── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── integration.json
│ │ │ └── hacs-test-org/
│ │ │ ├── addon-basic/
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── addon-basic.json
│ │ │ ├── appdaemon-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── apps/
│ │ │ │ │ │ └── example.json
│ │ │ │ │ ├── apps.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── appdaemon-basic.json
│ │ │ ├── integration-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ └── example/
│ │ │ │ │ │ └── manifest.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ ├── releases/
│ │ │ │ │ └── latest.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-basic-custom/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ └── example/
│ │ │ │ │ │ └── manifest.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-basic-custom.json
│ │ │ ├── integration-basic.json
│ │ │ ├── integration-invalid/
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-invalid.json
│ │ │ ├── plugin-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── plugin-basic.json
│ │ │ ├── plugin-custom-dist/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── plugin-custom-dist.json
│ │ │ ├── python_script-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── python_script-basic.json
│ │ │ ├── template-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── template-basic.json
│ │ │ ├── theme-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ └── theme-basic.json
│ │ ├── brands.home-assistant.io/
│ │ │ └── domains.json
│ │ ├── data-v2.hacs.xyz/
│ │ │ ├── appdaemon/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── critical/
│ │ │ │ └── data.json
│ │ │ ├── integration/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── netdaemon/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── plugin/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── python_script/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── removed/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── template/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ └── theme/
│ │ │ ├── data.json
│ │ │ └── repositories.json
│ │ ├── github.com/
│ │ │ └── hacs-test-org/
│ │ │ ├── appdaemon-basic/
│ │ │ │ └── _base/
│ │ │ │ ├── README.md
│ │ │ │ └── apps/
│ │ │ │ └── example/
│ │ │ │ └── __init__.py
│ │ │ └── integration-basic/
│ │ │ └── _base/
│ │ │ ├── README.md
│ │ │ └── custom_components/
│ │ │ └── example/
│ │ │ └── manifest.json
│ │ └── raw.githubusercontent.com/
│ │ └── hacs-test-org/
│ │ ├── appdaemon-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ └── 2.0.0/
│ │ │ ├── README.md
│ │ │ └── hacs.json
│ │ ├── integration-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ ├── 2.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ └── main/
│ │ │ └── hacs.json
│ │ ├── plugin-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── hacs.json
│ │ │ │ └── plugin-basic.js
│ │ │ └── 2.0.0/
│ │ │ ├── hacs.json
│ │ │ └── plugin-basic.js
│ │ ├── python_script-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ ├── hacs.json
│ │ │ │ └── python_scripts/
│ │ │ │ └── example.py
│ │ │ └── 2.0.0/
│ │ │ ├── README.md
│ │ │ ├── hacs.json
│ │ │ └── python_scripts/
│ │ │ └── example.py
│ │ ├── template-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── example.jinja
│ │ │ │ └── hacs.json
│ │ │ └── 2.0.0/
│ │ │ ├── example.jinja
│ │ │ └── hacs.json
│ │ └── theme-basic/
│ │ ├── 1.0.0/
│ │ │ ├── hacs.json
│ │ │ └── themes/
│ │ │ └── example.yaml
│ │ └── 2.0.0/
│ │ ├── hacs.json
│ │ └── themes/
│ │ └── example.yaml
│ ├── repository_data.json
│ ├── stored_repositories.json
│ ├── v2-appdaemon-data.json
│ ├── v2-critical-data.json
│ ├── v2-integration-data.json
│ ├── v2-plugin-data.json
│ ├── v2-python_script-data.json
│ ├── v2-removed-data.json
│ ├── v2-template-data.json
│ └── v2-theme-data.json
├── hacsbase/
│ ├── test_backup.py
│ ├── test_configuration.py
│ ├── test_hacs.py
│ └── test_hacsbase_data.py
├── helpers/
│ ├── classes/
│ │ ├── test_repository_data.py
│ │ └── test_validate_class.py
│ ├── download/
│ │ ├── test_gather_files_to_download.py
│ │ └── test_should_try_releases.py
│ ├── filters/
│ │ ├── test_filter_content_return_one_of_type.py
│ │ └── test_get_first_directory_in_directory.py
│ └── functions/
│ └── test_extract_repository_from_url.py
├── homeassistantfixtures/
│ ├── __init__.py
│ ├── common.py
│ ├── dev.py
│ └── min.py
├── integration/
│ └── test_integration_setup.py
├── patch_time.py
├── repositories/
│ ├── helpers/
│ │ ├── __init__.py
│ │ └── test_properties.py
│ ├── test_can_install.py
│ ├── test_display_status.py
│ ├── test_download_repository.py
│ ├── test_get_documentation.py
│ ├── test_get_hacs_json.py
│ ├── test_get_hacs_json_raw.py
│ ├── test_get_reposiotry_releases.py
│ ├── test_hacs_manifest.py
│ ├── test_plugin_repository.py
│ ├── test_register_repository.py
│ ├── test_remove_repository.py
│ ├── test_removed_repository.py
│ └── test_update_repository.py
├── ruff.toml
├── scripts/
│ └── data/
│ └── test_generate_category_data.py
├── snapshots/
│ ├── action/
│ │ └── test_hacs_action_integration/
│ │ ├── bad_documentation.log
│ │ ├── bad_issue_tracker.log
│ │ ├── no_releases.log
│ │ ├── releases_without_assets.log
│ │ └── valid_manifest.log
│ ├── api-usage/
│ │ └── tests/
│ │ ├── action/
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-no-releases.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json
│ │ │ └── test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json
│ │ ├── hacsbase/
│ │ │ ├── test_backuptest-directory.json
│ │ │ ├── test_backuptest-file.json
│ │ │ ├── test_backuptest-muilti.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-async-write1.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-async-write2.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-restore-write-not-new.json
│ │ │ ├── test_hacstest-add-remove-repository.json
│ │ │ └── test_hacstest-hacs.json
│ │ ├── helpers/
│ │ │ └── download/
│ │ │ ├── test_gather_files_to_downloadtest-gather-appdaemon-files-base.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-appdaemon-files-with-subdir.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-content-in-root-theme.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-files-to-download.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-different-card-name.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-dist.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-release-multiple.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-release.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-root.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-multiple-files-in-root.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-multiple-plugin-files-from-dist.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-zip-release.json
│ │ │ ├── test_gather_files_to_downloadtest-single-file-repo.json
│ │ │ ├── test_should_try_releasestest-base.json
│ │ │ ├── test_should_try_releasestest-category-is-wrong.json
│ │ │ ├── test_should_try_releasestest-no-releases.json
│ │ │ ├── test_should_try_releasestest-ref-is-default.json
│ │ │ └── test_should_try_releasestest-zip-release.json
│ │ ├── integration/
│ │ │ └── test_integration_setuptest-integration-setup.json
│ │ ├── repositories/
│ │ │ ├── helpers/
│ │ │ │ ├── test_propertiestest-repository-helpers-properties-can-be-installed.json
│ │ │ │ └── test_propertiestest-repository-helpers-properties-pending-update.json
│ │ │ ├── test_can_installtest-hacs-can-install.json
│ │ │ ├── test_display_statustest-display-status.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data0.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data1.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data2.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data3.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-1-0-0-expected0.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-99-99-99-none.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-with-exception.json
│ │ │ ├── test_get_hacs_jsontest-get-hacs-json-with-exception.json
│ │ │ ├── test_get_hacs_jsontest-validate-repository-1-0-0-integration-basic-1-0-0.json
│ │ │ ├── test_get_hacs_jsontest-validate-repository-99-99-99-none.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-integration-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-plugin-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-python-script-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-template-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-theme-basic.json
│ │ │ ├── test_plugin_repositorytest-add-dashboard-resource-with-invalid-file-name.json
│ │ │ ├── test_plugin_repositorytest-add-dashboard-resource.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-1-0-0-none-none-100.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-1-7-dev09-r2-none-none-17092.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-2-0-1-none-201.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-none-3-4-2-342.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-none-none.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-awesome-plugin-hacsfiles-awesome-plugin.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-plugin-advanced-hacsfiles-plugin-advanced.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-plugin-basic-hacsfiles-plugin-basic.json
│ │ │ ├── test_plugin_repositorytest-dashboard-url.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-hass-data.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-lovelace-data.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-lovelace-resources.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-store.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-none-store.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-wrong-key.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-wrong-version.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler.json
│ │ │ ├── test_plugin_repositorytest-remove-dashboard-resource.json
│ │ │ ├── test_plugin_repositorytest-update-dashboard-resource.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hacs-test-org-addon-basic-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hacs-test-org-integration-invalid-integration-hacs-test-org-integration-invalid-repository-structure-for-main-is-not-compliant.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hassio-addons-example-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-home-assistant-addons-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-home-assistant-core-you-can-not-add-homeassistant-core-to-use-core-integrations-check-the-home-assistant-documentation-for-how-to-add-them.json
│ │ │ ├── test_register_repositorytest-register-repository-hacs-test-org-integration-basic-custom-integration.json
│ │ │ ├── test_register_repositorytest-register-repository-hacs-test-org-plugin-custom-dist-plugin.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-download-failure.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-integration-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-plugin-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-python-script-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-template-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-theme-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-no-manifest.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-no-update.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-old-core-version.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-old-hacs-version.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-same-provided-version.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-integration-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-plugin-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-python-script-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-template-basic.json
│ │ │ └── test_update_repositorytest-update-repository-websocket-hacs-test-org-theme-basic.json
│ │ ├── scripts/
│ │ │ └── data/
│ │ │ ├── test_generate_category_datatest-generate-category-data-error-status-release-304-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-error-status-release-404-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-cancellederror-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-error2-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-timeouterror-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-template-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-theme-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-30plus-prereleases-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-template-basic.json
│ │ │ └── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-theme-basic.json
│ │ ├── test_config_flowtest-flow-with-activation-failure.json
│ │ ├── test_config_flowtest-flow-with-registration-failure.json
│ │ ├── test_config_flowtest-flow-with-remove-while-activating.json
│ │ ├── test_config_flowtest-full-user-flow-implementation.json
│ │ ├── test_config_flowtest-options-flow.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-appdaemon-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-integration-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-plugin-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-python-script-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-template-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-theme-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-appdaemon-data0.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-critical-data6.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-integration-data1.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-plugin-data2.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-python-script-data3.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-removed-data7.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-template-data4.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-theme-data5.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-appdaemon-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-integration-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-plugin-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-python-script-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-template-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-theme-basic.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-appdaemon-data0.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-integration-data1.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-plugin-data2.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-python-script-data3.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-template-data4.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-theme-data5.json
│ │ ├── test_data_clienttest-exception-handling-exception-error-fetching-data-from-hacs-test.json
│ │ ├── test_data_clienttest-exception-handling-timeouterror-timeout-of-60s-reached.json
│ │ ├── test_data_clienttest-status-handling-1009-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-200-does-not-raise.json
│ │ ├── test_data_clienttest-status-handling-201-does-not-raise.json
│ │ ├── test_data_clienttest-status-handling-301-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-302-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-304-hacsnotmodifiedexception.json
│ │ ├── test_data_clienttest-status-handling-400-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-401-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-403-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-418-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-429-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-500-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-529-hacsexception.json
│ │ ├── test_diagnosticstest-diagnostics-with-exception.json
│ │ ├── test_diagnosticstest-diagnostics.json
│ │ ├── test_sensor_cleanuptest-sensor-cleanup.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-appdaemon-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-integration-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-plugin-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-python-script-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-template-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-theme-basic.json
│ │ ├── test_system_healthtest-system-health-after-unload.json
│ │ ├── test_system_healthtest-system-health.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-appdaemon-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-integration-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-plugin-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-python-script-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-template-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-theme-basic.json
│ │ ├── utils/
│ │ │ ├── test_pathtest-is-safe.json
│ │ │ ├── test_queue_managertest-queue-manager.json
│ │ │ └── test_versiontest-version-to-download.json
│ │ └── validate/
│ │ ├── test_async_run_repository_checkstest-async-run-repository-checks.json
│ │ ├── test_brands_checktest-added-to-brands.json
│ │ ├── test_brands_checktest-local-brands-asset-content-in-root.json
│ │ ├── test_brands_checktest-local-brands-asset-missing-falls-back-to-remote.json
│ │ ├── test_brands_checktest-local-brands-asset-not-in-root.json
│ │ ├── test_brands_checktest-not-added-to-brands.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-integration-zip-release-with-filename.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-no-manifest.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-invalid-manifest.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-missing-filename.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-valid-manifest.json
│ │ ├── test_images_checktest-repository-has-images.json
│ │ ├── test_images_checktest-repository-has-not-images.json
│ │ ├── test_integration_manifest_checktest-hacs-manifest-with-invalid-manifest.json
│ │ ├── test_integration_manifest_checktest-integration-manifest-with-valid-manifest.json
│ │ ├── test_integration_manifest_checktest-integration-no-manifest.json
│ │ ├── test_repository_archived_checktest-repository-archived.json
│ │ ├── test_repository_archived_checktest-repository-not-archived.json
│ │ ├── test_repository_description_checktest-repository-hacs-description.json
│ │ ├── test_repository_description_checktest-repository-no-description.json
│ │ ├── test_repository_information_file_checktest-has-info-file.json
│ │ ├── test_repository_information_file_checktest-has-info-md-file.json
│ │ ├── test_repository_information_file_checktest-has-readme-file.json
│ │ ├── test_repository_information_file_checktest-has-readme-md-file.json
│ │ ├── test_repository_information_file_checktest-no-info-file.json
│ │ ├── test_repository_information_file_checktest-no-readme-file.json
│ │ ├── test_repository_issues_checktest-repository-issues-enabled.json
│ │ ├── test_repository_issues_checktest-repository-issues-not-enabled.json
│ │ ├── test_repository_topics_checktest-repository-hacs-topics.json
│ │ └── test_repository_topics_checktest-repository-no-topics.json
│ ├── config_flow/
│ │ ├── test_already_configured.json
│ │ ├── test_flow_with_activation_failure.json
│ │ ├── test_flow_with_registration_failure.json
│ │ └── test_full_user_flow_implementation.json
│ ├── data_client/
│ │ └── base/
│ │ ├── data/
│ │ │ ├── appdaemon.json
│ │ │ ├── integration.json
│ │ │ ├── plugin.json
│ │ │ ├── python_script.json
│ │ │ ├── template.json
│ │ │ └── theme.json
│ │ ├── data_validate/
│ │ │ ├── appdaemon.json
│ │ │ ├── critical.json
│ │ │ ├── integration.json
│ │ │ ├── plugin.json
│ │ │ ├── python_script.json
│ │ │ ├── removed.json
│ │ │ ├── template.json
│ │ │ └── theme.json
│ │ └── repositories/
│ │ ├── appdaemon.json
│ │ ├── integration.json
│ │ ├── plugin.json
│ │ ├── python_script.json
│ │ ├── template.json
│ │ └── theme.json
│ ├── diagnostics/
│ │ ├── base.json
│ │ └── exception.json
│ ├── hacs-test-org/
│ │ ├── appdaemon-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── integration-basic/
│ │ │ ├── get_documentation/
│ │ │ │ ├── installed_false_last_version_2_0_0.md
│ │ │ │ ├── installed_false_last_version_99_99_99.md
│ │ │ │ ├── installed_true_installed_version_1_0_0.md
│ │ │ │ └── installed_true_installed_version_1_0_0_last_version_2_0_0.md
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── integration-basic-custom/
│ │ │ └── test_register_repository.json
│ │ ├── plugin-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── plugin-custom-dist/
│ │ │ └── test_register_repository.json
│ │ ├── python_script-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── template-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ └── theme-basic/
│ │ ├── test_discard_invalid_repo_data.json
│ │ ├── test_download_repository.json
│ │ ├── test_get_reposiotry_releases.json
│ │ ├── test_remove_repository_post.json
│ │ ├── test_remove_repository_pre.json
│ │ ├── test_switch/
│ │ │ └── entity_states.json
│ │ ├── test_update_entity_state.json
│ │ ├── test_update_repository_entity.json
│ │ └── test_update_repository_websocket.json
│ ├── scripts/
│ │ └── data/
│ │ ├── generate_category_data/
│ │ │ ├── appdaemon/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── integration/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── plugin/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── python_script/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── single/
│ │ │ │ ├── appdaemon/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── appdaemon-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── integration/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── integration-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── plugin/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── plugin-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── python_script/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── python_script-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── template/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── template-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ └── theme/
│ │ │ │ └── hacs-test-org/
│ │ │ │ └── theme-basic/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── template/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ └── theme/
│ │ │ ├── data.json
│ │ │ ├── repositories.json
│ │ │ └── summary.json
│ │ ├── test_generate_category_data_error_status_release/
│ │ │ └── integration/
│ │ │ ├── 304.json
│ │ │ └── 404.json
│ │ ├── test_generate_category_data_errors_release/
│ │ │ └── integration/
│ │ │ ├── CancelledError.json
│ │ │ ├── TimeoutError.json
│ │ │ └── error2.json
│ │ ├── test_generate_category_data_with_30plus_prereleases/
│ │ │ └── integration.json
│ │ └── test_generate_category_data_with_prior_content/
│ │ ├── appdaemon.json
│ │ ├── integration.json
│ │ ├── plugin.json
│ │ ├── python_script.json
│ │ ├── template.json
│ │ └── theme.json
│ ├── system_health/
│ │ ├── system_health.json
│ │ └── system_health_after_unload.json
│ ├── test_integration_setup.json
│ └── test_integration_setup_with_custom_updater.json
├── test_config_flow.py
├── test_data_client.py
├── test_diagnostics.py
├── test_emuns.py
├── test_sensor_cleanup.py
├── test_switch.py
├── test_system_health.py
├── test_update.py
├── utils/
│ ├── test_decorator.py
│ ├── test_fs_util.py
│ ├── test_path.py
│ ├── test_queue_manager.py
│ ├── test_store.py
│ ├── test_url.py
│ ├── test_validate.py
│ ├── test_version.py
│ └── test_workarounds.py
└── validate/
├── test_async_run_repository_checks.py
├── test_brands_check.py
├── test_hacsjson_check.py
├── test_images_check.py
├── test_integration_manifest_check.py
├── test_repository_archived_check.py
├── test_repository_description_check.py
├── test_repository_information_file_check.py
├── test_repository_issues_check.py
└── test_repository_topics_check.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeclimate.yml
================================================
---
engines:
duplication:
enabled: true
config:
languages:
- python
fixme:
enabled: true
radon:
enabled: true
ratings:
paths:
- "**.py"
exclude_paths:
- tests/
- action/
- scripts/
================================================
FILE: .codecov.yml
================================================
comment: false
codecov:
branch: main
coverage:
precision: 2
round: down
range: "60...100"
status:
patch: off
project:
default:
target: 50%
validate:
target: 100%
paths:
- custom_components/hacs/validate/
repositories:
target: 50%
paths:
- custom_components/hacs/repositories/
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
ignore:
- "tests"
================================================
FILE: .coveragerc
================================================
[run]
source = custom_components
omit =
# omit tests
tests/*
# omit scripts
scripts/update/*
[report]
exclude_lines =
if TYPE_CHECKING:
================================================
FILE: .devcontainer.json
================================================
{
"name": "hacs/integration",
"image": "mcr.microsoft.com/devcontainers/python:1-3.13",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant"
},
"0-8122": {
"label": "Auto-Forwarded - Other",
"onAutoForward": "ignore"
},
"8124-999999": {
"label": "Auto-Forwarded - Other",
"onAutoForward": "ignore"
}
},
"customizations": {
"extensions": [
"charliermarsh.ruff",
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance",
"GitHub.copilot"
],
"vscode": {
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.formatting.provider": "ruff",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.rulers": [
100
],
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports": "always"
},
"files.trimTrailingWhitespace": true
},
"extensions": [
"GitHub.copilot",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-vscode.makefile-tools",
"ryanluker.vscode-coverage-gutters"
]
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers/features/rust:1": {}
}
}
================================================
FILE: .dockerignore
================================================
*
!custom_components/hacs
!scripts
!action
!constraints.txt
!requirements_action.txt
================================================
FILE: .gitattributes
================================================
text eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/a_integration.yml
================================================
---
name: "Backend/Integration"
description: You use this when something is not doing what it's supposed to do.
labels: "issue:backend"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io//more-info/system-health#github-issues)"
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I'm running the newest version of HACS <https://github.com/hacs/integration/releases/latest>
required: true
- label: I have enabled debug logging for my installation.
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: I have read <https://hacs.xyz/docs/help/issues/>
required: true
- label: This issue is related to the backend (integration part) of HACS.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This is a bug and not a feature request.
required: true
- label: This issue is not a duplicate issue of currently [open](https://github.com/hacs/integration/issues) or issues [pending release](https://github.com/hacs/integration/issues?q=is%3Aissue+is%3Aclosed+sort%3Aupdated-desc+milestone%3Anext).
required: true
- type: textarea
attributes:
label: "Describe the issue"
description: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix, it is very important that you fill out this part, issues without it will be closed"
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://hacs.xyz/docs/use/troubleshooting/logs/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag or paste the diagnostics dump file here. (see https://hacs.xyz/docs/use/troubleshooting/diagnostics/ for info)"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/b_frontend.yml
================================================
---
name: "Frontend"
description: You use this when elements in the UI are not working correctly.
labels: "issue:frontend"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: markdown
attributes:
value: "## Installation details"
- type: input
attributes:
label: Web browser
description: The type of browser you are using
validations:
required: true
- type: input
attributes:
label: Web browser version
description: The version of the browser you are using
validations:
required: true
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io//more-info/system-health#github-issues)"
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I'm running the newest version of HACS <https://github.com/hacs/integration/releases/latest>
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: I have read <https://hacs.xyz/docs/help/issues/>
required: true
- label: This issue is related to the frontend of HACS.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This is a bug and not a feature request.
required: true
- label: This issue is not a duplicate issue of currently [open](https://github.com/hacs/integration/issues) or issues [pending release](https://github.com/hacs/integration/issues?q=is%3Aissue+is%3Aclosed+sort%3Aupdated-desc+milestone%3Anext).
required: true
- type: textarea
attributes:
label: Describe the issue
placeholder: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix, it is very important that you fill out this part, issues without it will be closed"
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Screenshots
placeholder: "Here you paste screenshots to showcase the issue."
validations:
required: true
- type: textarea
attributes:
label: "Javascript logs from your browser console"
render: text
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://hacs.xyz/docs/use/troubleshooting/logs/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag or paste the diagnostics dump file here. (see https://hacs.xyz/docs/use/troubleshooting/diagnostics/ for info)"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/c_bot.yml
================================================
---
name: "hacs-bot"
description: You use this when hacs-bot did something wrong.
labels: "issue:bot"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: textarea
attributes:
label: Describe the issue
placeholder: "A clear and concise description of what the issue is."
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have filled out the issue template to the best of my ability.
required: true
- label: I have read <https://hacs.xyz/docs/help/issues/>
required: true
- label: This issue is related to the HACS bot.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This is a bug and not a feature request.
required: true
- label: This issue is not a duplicate issue of currently [open](https://github.com/hacs/integration/issues) or issues [pending release](https://github.com/hacs/integration/issues?q=is%3Aissue+is%3Aclosed+sort%3Aupdated-desc+milestone%3Anext).
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: HACS Looks different
url: https://experimental.hacs.xyz/docs/use/dashboard/
about: HACS does not look like others/HACS not showing menu/HACS does not look like guides/youtube Im following
- name: Closed issues for next release
url: https://github.com/hacs/integration/issues?q=is%3Aissue+is%3Aclosed+sort%3Aupdated-desc+milestone%3Anext
about: Se closed issues that will be a part of the next release
- name: How to file an issue
url: https://hacs.xyz/docs/issues
about: Describes what an issue should contain
- name: HACS Documentation
url: https://hacs.xyz/
about: The documentation for HACS
- name: HACS Discord server
url: https://discord.gg/apgchf8
about: For questions about HACS
================================================
FILE: .github/ISSUE_TEMPLATE/d_documentation.yml
================================================
---
name: "Documentation"
description: You use this when something is wrong with the documentation.
labels: "issue:documentation"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: textarea
attributes:
label: Describe the issue
placeholder: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Screenshots
placeholder: "Here you paste screenshots to showcase the issue."
================================================
FILE: .github/ISSUE_TEMPLATE/e_action.yml
================================================
---
name: "HACS Action"
description: You use this when there is an issue with the HACS action.
labels: "issue:action"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: textarea
attributes:
label: Describe the issue
placeholder: "A clear and concise description of what the issue is."
validations:
required: true
- type: input
attributes:
label: Link to action run
validations:
required: true
- type: input
attributes:
label: Link to action configuration
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have filled out the issue template to the best of my ability.
required: true
- label: I have read <https://hacs.xyz/docs/help/issues/>
required: true
- label: This issue is related to the HACS action.
required: true
- label: This is a bug and not a feature request.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/f_addon.yml
================================================
---
name: "HACS Add-ons"
description: You use this when there is an issue with one of the HACS Add-ons
labels: "issue:addon"
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have filled out the issue template to the best of my ability.
required: true
- label: I have read <https://hacs.xyz/docs/help/issues/>
required: true
- label: This issue is related to one the HACS add-ons.
required: true
- label: This is a bug and not a feature request.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- type: dropdown
validations:
required: true
attributes:
label: Which Add-on are you reporting an issue for?
options:
- get
- type: textarea
attributes:
label: Describe the issue
placeholder: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Add-on logs
validations:
required: true
- type: textarea
attributes:
label: Supervisor logs
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag or paste the diagnostics dump file here. (see https://hacs.xyz/docs/use/troubleshooting/diagnostics/ for info)"
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/removal.yml
================================================
---
name: Request for repository removal
description: Flagging of repository that should be removed from HACS
labels: flag
body:
- type: markdown
attributes:
value: |
Learn how to submit an issue here https://hacs.xyz/docs/help/issues/
Before you open a new issue, search through the existing issues to see if others have had the same problem.
The issue template is not a suggestion, fill out everything that is asked.
- type: input
id: repo
attributes:
label: Repository
description: The repository that is requested to be removed (owner/repository)
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I understand that this form should only be used for repositories that needs to be removed from HACS
required: true
- label: I understand that a bug is not reason enough to have a repository removed
required: true
- label: The repository is currently shipped as a default repository in HACS
required: true
- label: I have tried to get the authors attention to the reason for removal
required: true
- type: textarea
attributes:
label: Why should this be removed?
placeholder: "Describe why the repository should be removed from HACS. If you are flagging a repository for removal without a good reason/description, the request will be closed"
validations:
required: true
- type: input
attributes:
label: Link to issue
description: The URL to the issue that shows an attempt to contact the author has been made
validations:
required: true
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
================================================
FILE: .github/pre-commit-config.yaml
================================================
repos:
- repo: local
hooks:
- id: codespell
name: Check code for common misspellings
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: codespell
args:
- --quiet-level=2
- --ignore-words-list=hass,ba,fo
- --skip=tests/fixtures/*
- id: isort
name: Sort imports
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: isort
- id: pyupgrade
name: Run pyupgrade
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: pyupgrade
files: ^.*.py$
args:
- "--py39-plus"
- id: ruff-check
name: Run ruff check
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: ruff
args:
- check
files: ^((action|custom_components|script|tests)/.+)?[^/]+\.py$
- id: ruff-format
name: Run ruff format
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: ruff
args:
- format
files: ^((action|custom_components|script)/.+)?[^/]+\.py$
- id: check-executables-have-shebangs
name: Check that executables have shebangs
language: system
types: [text, executable]
entry: check-executables-have-shebangs
stages: [pre-commit, pre-push, manual]
- id: check-json
name: Check JSON files
language: system
types: [json]
stages: [pre-commit, pre-push, manual]
entry: check-json
- id: requirements-txt-fixer
name: Check requirements files
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: requirements-txt-fixer
files: ^requirements_.*.txt$
- id: check-ast
name: Check Python AST
language: system
types: [python]
stages: [pre-commit, pre-push, manual]
entry: check-ast
- id: mixed-line-ending
name: Check line nedings
language: system
types: [text]
stages: [pre-commit, pre-push, manual]
entry: mixed-line-ending
args:
- --fix=lf
================================================
FILE: .github/release.yml
================================================
changelog:
categories:
- title: '💥 Breaking changes'
labels:
- 'Breaking Change'
- title: '🛎️ Experimental'
labels:
- 'Experimental'
- title: '✨ New features'
labels:
- 'pr: new-feature'
- title: '⚡ Enhancements'
labels:
- 'pr: enhancement'
- title: '♻️ Refactor'
labels:
- 'pr: refactor'
- title: '🐛 Bug fixes'
labels:
- 'pr: bugfix'
- title: '🎨 Frontend updates'
labels:
- 'pr: frontend-update'
================================================
FILE: .github/workflows/action-container.yml
================================================
name: "Build the action container"
on:
release:
types:
- published
push:
branches:
- main
paths:
- '.github/workflows/action-container.yml'
- 'custom_components/**'
- 'action/**'
pull_request:
branches:
- main
paths:
- '.github/workflows/action-container.yml'
- 'custom_components/**'
- 'action/**'
concurrency:
group: container-build-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
if: ${{ github.event_name != 'pull_request' }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
ghcr.io/${{ github.repository_owner }}/action
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- name: Build and push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: action/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/generate-hacs-data.yml
================================================
name: Generate HACS Data
on:
workflow_dispatch:
inputs:
forceRepositoryUpdate:
description: 'Force repository update'
required: false
default: 'False'
type: choice
options:
- "False"
- "True"
category:
description: 'Select a category'
required: false
type: choice
options:
- None
- appdaemon
- integration
- plugin
- python_script
- template
- theme
schedule:
- cron: "0 */2 * * *"
concurrency:
group: category-data
permissions: {}
jobs:
generate-matrix:
name: Generate matrix
runs-on: ubuntu-latest
if: github.repository == 'hacs/integration'
outputs:
categories: ${{ steps.set-matrix.outputs.categories }}
steps:
- id: set-matrix
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ "${{ inputs.category }}" != "None" ]] && [[ "${{ inputs.category }}" != "" ]]; then
echo "categories=['${{ inputs.category }}']" >> $GITHUB_OUTPUT
else
echo "categories=['appdaemon','integration','plugin','python_script','template','theme']" >> $GITHUB_OUTPUT
fi
category-data:
runs-on: ubuntu-latest
needs: generate-matrix
if: github.repository == 'hacs/integration'
name: Generate ${{ matrix.category }} data
strategy:
fail-fast: false
matrix:
category: ${{ fromJSON( needs.generate-matrix.outputs.categories )}}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: python
with:
python-version: "3.13"
cache: 'pip'
cache-dependency-path: |
requirements_base.txt
requirements_generate_data.txt
- name: Install dependencies
run: |
scripts/install/frontend
scripts/install/pip_packages --requirement requirements_generate_data.txt
- name: Generate ${{ matrix.category }} data
run: python3 -m scripts.data.generate_category_data ${{ matrix.category }}
env:
DATA_GENERATOR_TOKEN: ${{ secrets.DATA_GENERATOR_TOKEN }}
FORCE_REPOSITORY_UPDATE: ${{ inputs.forceRepositoryUpdate }}
- name: Validate output with JQ
run: |
jq -c . outputdata/${{ matrix.category }}/data.json
jq -c . outputdata/${{ matrix.category }}/repositories.json
- name: Validate output with schema
run: |
python3 -m scripts.data.validate_category_data ${{ matrix.category }} outputdata/${{ matrix.category }}/data.json
- name: Generate diff
run: |
diff -U 8 outputdata/diff/${{ matrix.category }}_before.json outputdata/diff/${{ matrix.category }}_after.json > outputdata/diff/${{ matrix.category }}.diff || true
cat outputdata/diff/${{ matrix.category }}.diff
- name: Upload diff
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CATEGORY: ${{ matrix.category }}
with:
script: |
const fs = require('fs');
const diffContents = fs.readFileSync(`outputdata/diff/${process.env.CATEGORY}.diff`);
core.summary.addDetails(`${process.env.CATEGORY}.diff contents`, `\n\n\`\`\`diff\n${diffContents}\`\`\`\n\n`)
core.summary.write()
- name: Upload artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.category }}
path: |
outputdata/${{ matrix.category }}
outputdata/summary.json
outputdata/diff
if-no-files-found: error
retention-days: 7
summarize:
name: Summarize
runs-on: ubuntu-latest
needs: category-data
if: github.repository == 'hacs/integration'
outputs:
changedCategories: ${{ steps.combined.outputs.changedCategories }}
environments: ${{ steps.combined.outputs.environments }}
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: outputdata
- name: Generate combined summary
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: combined
env:
HACS_CHANGED_PCT_TARGET: ${{ vars.HACS_CHANGED_PCT_TARGET }}
HACS_DIFF_TARGET: ${{ vars.HACS_DIFF_TARGET }}
with:
script: |
const fs = require('fs');
const summaries = {};
const changedCategories = [];
const environments = {};
const diffTarget = Number(process.env.HACS_DIFF_TARGET || 1)
core.info(`[global] diffTarget: ${diffTarget}`);
const subDirectories = fs.readdirSync("outputdata", { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
for (const directory of subDirectories) {
let changedPctTarget = Number(process.env.HACS_CHANGED_PCT_TARGET)
const parsed = JSON.parse(fs.readFileSync(`outputdata/${directory}/summary.json`))
if (!changedPctTarget) {
if (!parsed.new_count || parsed.new_count <= 0) {
changedPctTarget = 0;
} else if (parsed.new_count > 750) {
changedPctTarget = 7;
} else if (parsed.new_count > 500) {
changedPctTarget = 8;
} else if (parsed.new_count > 250) {
changedPctTarget = 9;
} else if (parsed.new_count > 100) {
changedPctTarget = 10;
} else if (parsed.new_count > 75) {
changedPctTarget = 15;
} else if (parsed.new_count > 50) {
changedPctTarget = 20;
} else if (parsed.new_count > 25) {
changedPctTarget = 25;
} else if (parsed.new_count > 10) {
changedPctTarget = 28;
} else {
changedPctTarget = 50;
}
}
core.info(`[${directory}] changedPctTarget: ${changedPctTarget}`);
if (parsed.changed >= 1 || parsed.diff >= 1) {
changedCategories.push(directory)
}
if (parsed.changed_pct >= changedPctTarget) {
core.warning(`${directory} changed ${parsed.changed_pct}%!`)
environments[directory] = `publish-${directory}-verify`;
}
if (parsed.diff >= diffTarget) {
core.warning(`${directory} changed ${parsed.diff}!`)
environments[directory] = `publish-${directory}-verify`;
}
summaries[directory] = JSON.parse(fs.readFileSync(`outputdata/${directory}/summary.json`));
}
core.summary.addCodeBlock(JSON.stringify({summaries, environments, changedCategories}, null, 4), "json")
core.summary.write()
core.setOutput("changedCategories", JSON.stringify(changedCategories))
core.setOutput("environments", environments)
- name: Send notification
if: ${{ steps.combined.outputs.environments != '{}' }}
run: |
curl \
-H "Content-Type: application/json" \
-d '{"username": "GitHub action", "content": "[Attention needed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})"}' \
${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }}
publish:
runs-on: ubuntu-latest
needs: summarize
if: github.repository == 'hacs/integration'
name: Publish ${{ matrix.category }} data
environment: ${{ fromJSON(needs.summarize.outputs.environments)[matrix.category] }}
strategy:
fail-fast: false
matrix:
category: ${{ fromJSON(needs.summarize.outputs.changedCategories) }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: python
with:
python-version: "3.13"
cache: 'pip'
cache-dependency-path: |
requirements_base.txt
requirements_generate_data.txt
- name: Install dependencies
run: |
scripts/install/frontend
scripts/install/pip_packages --requirement requirements_generate_data.txt
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.category }}
path: outputdata
- name: Validate output with JQ
run: |
jq -c . outputdata/${{ matrix.category }}/data.json
jq -c . outputdata/${{ matrix.category }}/repositories.json
- name: Validate output with schema
run: |
python3 -m scripts.data.validate_category_data ${{ matrix.category }} outputdata/${{ matrix.category }}/data.json
- name: Upload to R2
run: |
aws s3 sync \
outputdata/${{ matrix.category }} \
s3://data-v2/${{ matrix.category }} \
--endpoint-url ${{ secrets.CF_R2_ENDPOINT_DATA }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
- name: Bust Cloudflare cache
run: |
curl --silent --show-error --fail -X POST \
"https://api.cloudflare.com/client/v4/zones/${{ secrets.CF_ZONE_ID }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CF_BUST_CACHE_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"files": ["https:\/\/data-v2.hacs.xyz\/${{ matrix.category }}\/data.json", "https:\/\/data-v2.hacs.xyz\/${{ matrix.category }}\/repositories.json"]}'
notify_on_failure:
runs-on: ubuntu-latest
name: Trigger Discord notification when jobs fail
needs: ["generate-matrix", "category-data", "summarize", "publish"]
steps:
- name: Send notification
if: ${{ always() && contains(join(needs.*.result, ','), 'failure') && github.event_name == 'schedule' }}
run: |
curl \
-H "Content-Type: application/json" \
-d '{"username": "GitHub action failure", "content": "[Scheduled action failed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})"}' \
${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }}
================================================
FILE: .github/workflows/lint.yaml
================================================
name: Lint
on:
pull_request:
branches:
- main
push:
branches:
- main
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
matrix:
runs-on: ubuntu-latest
name: Run ${{ matrix.check }}
strategy:
matrix:
check:
- check-ast
- check-executables-have-shebangs
- check-json
- codespell
- isort
- mixed-line-ending
- pyupgrade
- requirements-txt-fixer
- ruff-check
- ruff-format
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: python
with:
python-version: "3.13"
cache: 'pip'
cache-dependency-path: |
requirements_base.txt
requirements_lint.txt
- name: Install dependencies
run: |
scripts/install/pip_packages --requirement requirements_lint.txt
pre-commit install-hooks --config .github/pre-commit-config.yaml
- name: Run the check (${{ matrix.check }})
run: pre-commit run --show-diff-on-failure --hook-stage manual ${{ matrix.check }} --all-files --config .github/pre-commit-config.yaml
lint-json:
runs-on: ubuntu-latest
name: With JQ
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run validation
run: jq -r -e -c . tests/fixtures/*.json
================================================
FILE: .github/workflows/lock.yml
================================================
name: "Lock closed issues and PR's"
on:
schedule:
- cron: "0 * * * *"
concurrency:
group: lock-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
lock:
runs-on: ubuntu-latest
if: github.repository == 'hacs/integration'
permissions:
issues: write
pull-requests: write
steps:
- name: 🔒 Lock closed issues and PRs
uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
issue-inactive-days: "14"
issue-lock-reason: ""
pr-inactive-days: "1"
pr-lock-reason: ""
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
release:
types:
- published
push:
branches:
- main
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
release_zip_file:
name: Publish HACS zip file asset
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🛠️ Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: 🔢 Get version
if: ${{ github.event_name == 'release' }}
id: version
uses: home-assistant/actions/helpers/version@dce0e860c68256ef2902ece06afa5401eb4674e1 # master
- name: 🔢 Set version number
if: ${{ github.event_name == 'release' }}
run: |
sed -i "/MINIMUM_HA_VERSION = /c\MINIMUM_HA_VERSION = \"$(jq .homeassistant -r ${{ github.workspace }}/hacs.json)\"" ${{ github.workspace }}/custom_components/hacs/const.py
python3 ${{ github.workspace }}/scripts/update/manifest.py --version ${{ steps.version.outputs.version }}
- name: ⏬ Download HACS frontend
run: ${{ github.workspace }}/scripts/install/frontend
- name: 📤 Upload zip to action
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ github.event_name == 'push' }}
with:
name: hacs
path: ${{ github.workspace }}/custom_components/hacs
retention-days: 7
# Pack the HACS dir as a zip and upload to the release
- name: 📦 ZIP HACS Dir
if: ${{ github.event_name == 'release' }}
run: |
cd ${{ github.workspace }}/custom_components/hacs
zip hacs.zip -r ./
- name: 📤 Upload zip to release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
if: ${{ github.event_name == 'release' }}
with:
files: ${{ github.workspace }}/custom_components/hacs/hacs.zip
================================================
FILE: .github/workflows/pull_requests_labels.yml
================================================
name: "Check Pull Request labels"
on:
pull_request:
types:
- labeled
- opened
- synchronize
- unlabeled
branches:
- main
permissions: {}
jobs:
check_labels:
name: "Check Pull Request labels"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check the labels
uses: ludeeus/action-require-labels@7ef0dba93830452589680da7cdea2e2c4c0f8dff # 1.1.0
with:
labels: >-
Breaking Change, Experimental, pr: new-feature,
pr: enhancement, pr: refactor, pr: bugfix,
pr: dependency-update, pr: action, pr: test,
pr: repository
================================================
FILE: .github/workflows/pytest.yml
================================================
name: Test
on:
pull_request:
branches:
- main
push:
branches:
- main
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
legacy:
name: With pytest with Home Assistant (min. supported version)
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🛠️ Set up Python 3.13
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
cache: 'pip'
cache-dependency-path: |
requirements_core_min.txt
requirements_base.txt
requirements_test.txt
- name: 📦 Install dependencies
run: |
scripts/install/pip_packages \
--requirement requirements_core_min.txt \
--requirement requirements_test.txt
scripts/install/frontend
- name: ⏲️ Set time zone
uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0
with:
timezoneLinux: 'Asia/Singapore'
- name: 🏃 Run tests
env:
PYTEST: true
run: scripts/test
dev:
name: With pytest with Home Assistant (${{ matrix.homeassistant-version }}) & Python (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
homeassistant-version:
- "dev"
python-version:
- "3.14"
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🛠️ Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: |
requirements_core_min.txt
requirements_base.txt
requirements_test.txt
- name: 📦 Install dependencies
run: |
scripts/install/pip_packages --requirement requirements_test.txt
scripts/install/core_dev
scripts/install/frontend
- name: ⏲️ Set time zone
uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0
with:
timezoneLinux: 'Asia/Singapore'
- name: 🏃 Run tests
env:
PYTEST: true
run: scripts/test
- name: 📤 Upload coverage to Codecov
if: ${{ matrix.python-version == '3.14' }}
run: |
scripts/coverage
curl -sfSL https://codecov.io/bash | bash -
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale issues'
on:
workflow_dispatch:
schedule:
- cron: '30 10 * * *'
permissions: {}
jobs:
issues_missing_required_information:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
pull-requests: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
days-before-stale: -1
days-before-close: 7
only-labels: 'Missing required issue information'
stale-issue-label: 'Missing required issue information'
================================================
FILE: .github/workflows/validate.yml
================================================
name: Validate
on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
- cron: "0 12 * * *"
concurrency:
cancel-in-progress: true
group: validate-${{ github.ref }}
permissions: {}
jobs:
preflight:
if: ${{ github.repository == 'hacs/integration' }}
runs-on: ubuntu-latest
name: Preflight
steps:
- name: Validation preflight
env:
ACTOR: ${{ github.actor }}
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
run: |
echo "**Start:** $(date)" >> $GITHUB_STEP_SUMMARY
echo "**Actor:** $ACTOR" >> $GITHUB_STEP_SUMMARY
echo "**Event:** $EVENT_NAME" >> $GITHUB_STEP_SUMMARY
echo "**Ref name:** $REF_NAME" >> $GITHUB_STEP_SUMMARY
echo "**Ref:** $REF" >> $GITHUB_STEP_SUMMARY
echo "**SHA:** $SHA" >> $GITHUB_STEP_SUMMARY
validate-hassfest:
needs:
- "preflight"
runs-on: ubuntu-latest
name: With hassfest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Test files conflict with running hassfest
- name: Remove tests
run: rm -rf tests
- name: Hassfest validation
uses: home-assistant/actions/hassfest@dce0e860c68256ef2902ece06afa5401eb4674e1 # master
validate-hacs:
needs:
- "preflight"
runs-on: ubuntu-latest
name: With HACS Action
steps:
- name: HACS validation
uses: hacs/action@dcb30e72781db3f207d5236b861172774ab0b485 # main
with:
category: integration
validate-hacs-local:
if: ${{ github.event_name != 'schedule' }}
needs:
- "preflight"
runs-on: ubuntu-latest
name: Check ${{matrix.entry.category}} ${{matrix.entry.repository}} with HACS Action (local)
strategy:
matrix:
entry:
- repository: "hacs/integration"
category: "integration"
- repository: "piitaya/lovelace-mushroom"
category: "plugin"
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Container
run: |
docker build . -t hacs/action:local -f action/Dockerfile
- name: Run Action
run: |
docker run --name hacs_action_local \
--env INPUT_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
--env INPUT_REPOSITORY=${{matrix.entry.repository}} \
--env INPUT_CATEGORY=${{matrix.entry.category}} \
hacs/action:local
validata-hacs-data:
if: ${{ github.event_name != 'schedule' }}
needs:
- "preflight"
runs-on: ubuntu-latest
name: Check ${{matrix.entry.category}} ${{matrix.entry.repository}} with HACS data generation
strategy:
matrix:
entry:
- repository: "hacs/integration"
category: "integration"
- repository: "piitaya/lovelace-mushroom"
category: "plugin"
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
cache: 'pip'
cache-dependency-path: |
requirements_base.txt
requirements_generate_data.txt
- name: Install dependencies
run: |
scripts/install/frontend
scripts/install/pip_packages --requirement requirements_generate_data.txt
- name: Generate data
run: |
python3 -m scripts.data.generate_category_data \
${{ matrix.entry.category }} \
${{ matrix.entry.repository }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate diff
run: |
diff -U 8 outputdata/diff/${{ matrix.entry.category }}_before.json outputdata/diff/${{ matrix.entry.category }}_after.json > outputdata/diff/${{ matrix.entry.category }}.diff || true
cat outputdata/diff/${{ matrix.entry.category }}.diff
- name: Upload diff
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CATEGORY: ${{ matrix.entry.category }}
with:
script: |
const fs = require('fs');
const diffContents = fs.readFileSync(`outputdata/diff/${process.env.CATEGORY}.diff`);
core.summary.addDetails(`${process.env.CATEGORY}.diff contents`, `\n\n\`\`\`diff\n${diffContents}\`\`\`\n\n`)
core.summary.write()
- name: Validate output with JQ
run: |
jq -c . outputdata/${{ matrix.entry.category }}/data.json
jq -c . outputdata/${{ matrix.entry.category }}/repositories.json
- name: Validate output with schema
run: |
python3 -m scripts.data.validate_category_data ${{ matrix.entry.category }} outputdata/${{ matrix.entry.category }}/data.json
- name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: "${{ matrix.entry.category }}_${{ strategy.job-index }}"
path: |
outputdata/summary.json
outputdata/${{ matrix.entry.category }}
outputdata/diff
if-no-files-found: error
retention-days: 3
notify_on_failure:
runs-on: ubuntu-latest
name: Trigger Discord notification when jobs fail
needs: ["preflight","validate-hassfest", "validate-hacs"]
steps:
- name: Send notification
if: ${{ always() && contains(join(needs.*.result, ','), 'failure') && github.event_name == 'schedule' }}
run: |
curl \
-H "Content-Type: application/json" \
-d '{"username": "GitHub action failure", "content": "[Scheduled action failed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})"}' \
${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }}
================================================
FILE: .gitignore
================================================
# artifacts
__pycache__
.pytest*
*.egg-info
*/build/*
*/dist/*
# misc
.claude
.coverage
.python-version
.venv
.vscode
coverage.xml
htmlcov
outputdata
settings.json
venv
# Frontend are downloaded on release
custom_components/hacs/hacs_frontend
# Translation files
custom_components/hacs/translations
!custom_components/hacs/translations/en.json
# Home Assistant configuration
config
================================================
FILE: .pylintrc
================================================
[MESSAGES CONTROL]
# pylint issue with Python 3.9 https://github.com/PyCQA/pylint/issues/3882
disable=unsubscriptable-object
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 - 2023 Joakim Sørensen (@ludeeus)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# HACS (Home Assistant Community Store)
_Manage (Install, track, upgrade) and discover custom elements for Home Assistant directly from the UI._
## What?
HACS is an integration that gives the user a powerful UI to handle downloads of custom needs.
**Highlights of what HACS can do:**
- Help you discover new custom elements.
- Help you download new custom elements.
- Help you keep track of your custom elements.
- Manage(download/update/remove)
- Shortcuts to repositories/issue tracker
## Useful links
- [General documentation](https://hacs.xyz/)
- [Configuration](https://hacs.xyz/docs/use/configuration/basic)
- [FAQ](https://hacs.xyz/docs/faq)
- [GitHub](https://github.com/hacs)
- [Discord](https://discord.gg/apgchf8)
- [Become a GitHub sponsor? ❤️](https://github.com/sponsors/ludeeus)
- [BuyMe~~Coffee~~Beer? 🍺🙈](https://buymeacoffee.com/ludeeus)
## Issues
~~If~~ When you experience issues/bugs with this the best way to report them is to open an issue in **this** repo.
[Issue link](https://hacs.xyz/docs/help/issues)
================================================
FILE: action/Dockerfile
================================================
FROM python:3.13-alpine
WORKDIR /hacs
COPY . /hacs
ENV \
UV_SYSTEM_PYTHON=true \
UV_EXTRA_INDEX_URL="https://wheels.home-assistant.io/musllinux-index/"
RUN \
apk add --no-cache --virtual .build-deps \
bash \
\
&& bash /hacs/scripts/install/pip_packages \
uv==0.9.6 \
\
&& bash /hacs/scripts/install/uv_packages \
-r requirements_action.txt \
\
&& bash /hacs/scripts/install/frontend \
\
&& apk del --no-cache .build-deps > /dev/null 2>&1 \
\
&& rm -rf /var/cache/apk/* \
\
&& find /usr/local \( -type d -a -name test -o -name tests -o -name '__pycache__' \) -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) -exec rm -rf '{}' \; \
\
&& mv /hacs/action/action.py /hacs/action.py \
\
&& rm -rf /hacs/scripts /hacs/action \
\
&& rm /hacs/requirements_action.txt /hacs/constraints.txt
ENTRYPOINT ["python3", "/hacs/action.py"]
================================================
FILE: action/action.py
================================================
"""Validate a GitHub repository to be used with HACS."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from aiogithubapi import GitHub, GitHubAPI
import aiohttp
from homeassistant.core import HomeAssistant
from custom_components.hacs.base import HacsBase
from custom_components.hacs.const import HACS_ACTION_GITHUB_API_HEADERS
from custom_components.hacs.enums import HacsGitHubRepo
from custom_components.hacs.exceptions import HacsException
from custom_components.hacs.utils.decode import decode_content
from custom_components.hacs.utils.logger import LOGGER
from custom_components.hacs.validate.manager import ValidationManager
TOKEN = os.getenv("INPUT_GITHUB_TOKEN")
GITHUB_WORKSPACE = os.getenv("GITHUB_WORKSPACE")
GITHUB_ACTOR = os.getenv("GITHUB_ACTOR")
GITHUB_EVENT_PATH = os.getenv("GITHUB_EVENT_PATH")
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",
"plugin",
"python_script",
"template",
"theme",
]
logging.basicConfig(
format="::%(levelname)s:: %(message)s",
level=logging.DEBUG,
)
def error(error: str):
LOGGER.error(error)
exit(1)
def output_in_group(group: str, content: str):
print(f"::group::{group}") # noqa: T201
print(content) # noqa: T201
print("::endgroup::") # noqa: T201
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())
async def choose_repository(githubapi: GitHubAPI, category: str):
if category is None:
return None
response = await githubapi.repos.contents.get(HacsGitHubRepo.DEFAULT, category)
current = json.loads(decode_content(response.data.content))
with open(f"{GITHUB_WORKSPACE}/{category}") as cat_file: # noqa: ASYNC230
new = json.loads(cat_file.read())
for repo in current:
if repo in new:
new.remove(repo)
if len(new) != 1:
error(f"{new} is not a single repository")
return new[0]
def choose_category():
for name in CHANGED_FILES.split(" "):
if name in CATEGORIES:
return name
async def preflight():
"""Preflight checks."""
event_data = get_event_data()
ref: str | None = None
hacs = HacsBase()
hacs.hass = HomeAssistant("")
hacs.system.action = True
hacs.configuration.token = TOKEN
hacs.core.config_path = None
async with aiohttp.ClientSession() as session:
hacs.session = session
hacs.validation = ValidationManager(hacs=hacs, hass=hacs.hass)
hacs.githubapi = GitHubAPI(
token=hacs.configuration.token,
session=session,
client_name="HACS/Action",
)
if REPOSITORY and CATEGORY:
repository = REPOSITORY
category = CATEGORY
elif GITHUB_REPOSITORY == HacsGitHubRepo.DEFAULT:
category = choose_category()
repository = await choose_repository(hacs.githubapi, category)
LOGGER.info(f"Actor: {GITHUB_ACTOR}")
else:
category = CATEGORY.lower()
if event_data.get("pull_request") is not None:
head = event_data["pull_request"]["head"]
ref = head["ref"]
repository = head["repo"]["full_name"]
else:
repository = GITHUB_REPOSITORY
if event_data.get("ref") is not None:
# For push events
ref = event_data["ref"]
# For tag events
if ref.startswith("refs/tags/"):
ref = ref.split("/")[-1]
if TOKEN is None:
error("No GitHub token found, use env GITHUB_TOKEN to set this.")
if repository is None:
error("No repository found, use env REPOSITORY to set this.")
if category is None:
error("No category found, use env CATEGORY to set this.")
if category not in CATEGORIES:
error(f"Category {category} is not valid.")
if (repository_ref := os.getenv("REPOSITORY_REF")) is not None:
ref = repository_ref
if ref is None and GITHUB_REPOSITORY != HacsGitHubRepo.DEFAULT:
repo = await hacs.githubapi.repos.get(repository)
ref = repo.data.default_branch
LOGGER.info(f"Category: {category}")
LOGGER.info(f"Repository: {repository}{f'@{ref}' if ref else ''}")
await validate_repository(hacs, repository, category, ref)
async def validate_repository(hacs: HacsBase, repository: str, category: str, ref=None):
"""Validate."""
# Legacy GitHub client
hacs.github = GitHub(
hacs.configuration.token,
hacs.session,
headers=HACS_ACTION_GITHUB_API_HEADERS,
)
try:
await hacs.async_register_repository(
repository_full_name=repository,
category=category,
ref=ref,
)
except HacsException as exception:
LOGGER.error(exception)
if (repo := hacs.repositories.get_by_full_name(repository)) is None:
error(f"Repository {repository} not loaded properly in HACS.")
output_in_group(
"data",
json.dumps(
{
"data": repo.data.to_json(),
"manifest": repo.repository_manifest.to_dict(),
"release": (
{
"tag": matching_release.tag_name,
"assets": [asset.name for asset in matching_release.assets],
}
if repo.releases.objects
and len(repo.releases.objects) > 0
and (
matching_release := next(
(
release
for release in repo.releases.objects
if release.tag_name == repo.data.last_version
),
repo.releases.objects[0],
)
)
else None
),
"category": category,
"ref": ref,
},
indent=4,
),
)
if __name__ == "__main__":
asyncio.run(preflight())
================================================
FILE: constraints.txt
================================================
================================================
FILE: custom_components/hacs/__init__.py
================================================
"""HACS gives you a powerful UI to handle downloads of all your custom needs.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
from __future__ import annotations
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration
from .base import HacsBase
from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION
from .data_client import HacsDataClient
from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend
from .utils.data import HacsData
from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands
PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
async def _async_initialize_integration(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs()
if config_entry.source == SOURCE_IMPORT:
# Import is not supported
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
**config_entry.data,
**config_entry.options,
},
)
integration = await async_get_integration(hass, DOMAIN)
hacs.set_stage(None)
hacs.log.info("Starting HACS[%s]", integration.version)
clientsession = async_get_clientsession(hass)
hacs.integration = integration
hacs.version = integration.version
hacs.configuration.dev = integration.version == "0.0.0"
hacs.hass = hass
hacs.queue = QueueManager(hass=hass)
hacs.data = HacsData(hacs=hacs)
hacs.data_client = HacsDataClient(
session=clientsession,
client_name=f"HACS/{integration.version}",
)
hacs.system.running = True
hacs.session = clientsession
hacs.core.lovelace_mode = LovelaceMode.YAML
try:
lovelace_info = await system_health_info(hacs.hass)
hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml"))
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode
pass
hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None:
hacs.core.ha_version = AwesomeVersion(HAVERSION)
## Legacy GitHub client
hacs.github = GitHub(
hacs.configuration.token,
clientsession,
headers={
"User-Agent": f"HACS/{hacs.version}",
"Accept": ACCEPT_HEADERS["preview"],
},
)
## New GitHub client
hacs.githubapi = GitHubAPI(
token=hacs.configuration.token,
session=clientsession,
**{"client_name": f"HACS/{hacs.version}"},
)
async def async_startup():
"""HACS startup tasks."""
hacs.enable_hacs()
try:
import custom_components.custom_updater
except ImportError:
pass
else:
hacs.log.critical(
"HACS cannot be used with custom_updater. "
"To use HACS you need to remove custom_updater from `custom_components`",
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string,
MINIMUM_HA_VERSION,
):
hacs.log.critical(
"You need HA version %s or newer to use this integration.",
MINIMUM_HA_VERSION,
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not await hacs.data.restore():
hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False
hacs.set_active_categories()
async_register_websocket_commands(hass)
await async_register_frontend(hass, hacs)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled:
return False
hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
return not hacs.system.disabled
async def async_try_startup(_=None):
"""Startup wrapper for yaml config."""
try:
startup_result = await async_startup()
except AIOGitHubAPIException:
startup_result = False
if not startup_result:
if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup)
return
hacs.enable_hacs()
await async_try_startup()
# Remove old (v0-v1) sensor if it exists, can be removed in v3
er = async_get_entity_registry(hass)
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
er.async_remove(old_sensor)
# Mischief managed!
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
hacs: HacsBase = hass.data[DOMAIN]
if hacs.queue.has_pending_tasks:
hacs.log.warning("Pending tasks, can not unload, try again later.")
return False
# Clear out pending queue
hacs.queue.clear()
for task in hacs.recurring_tasks:
# Cancel all pending tasks
task()
# Store data
await hacs.data.async_write(force=True)
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
async_remove_panel(hass, "hacs")
except AttributeError:
pass
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED)
hass.data.pop(DOMAIN, None)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload the HACS config entry."""
if not await async_unload_entry(hass, config_entry):
return
await async_setup_entry(hass, config_entry)
================================================
FILE: custom_components/hacs/base.py
================================================
"""Base HACS class."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from datetime import timedelta
import gzip
import math
import os
import pathlib
import shutil
from typing import TYPE_CHECKING, Any
from aiogithubapi import (
AIOGitHubAPIException,
GitHub,
GitHubAPI,
GitHubAuthenticationException,
GitHubException,
GitHubNotModifiedException,
GitHubRatelimitException,
)
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
from aiohttp.client import ClientSession, ClientTimeout
from awesomeversion import AwesomeVersion
from homeassistant.components.persistent_notification import (
async_create as async_create_persistent_notification,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.loader import Integration
from homeassistant.util import dt
from .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient
from .enums import (
HacsCategory,
HacsDisabledReason,
HacsDispatchEvent,
HacsGitHubRepo,
HacsStage,
LovelaceMode,
)
from .exceptions import (
AddonRepositoryException,
HacsException,
HacsExecutionStillInProgress,
HacsExpectedException,
HacsNotModifiedException,
HacsRepositoryArchivedException,
HacsRepositoryExistException,
HomeAssistantCoreRepositoryException,
)
from .repositories import REPOSITORY_CLASSES
from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT
from .utils.file_system import async_exists
from .utils.json import json_loads
from .utils.logger import LOGGER
from .utils.queue_manager import QueueManager
from .utils.store import async_load_from_store, async_save_to_store
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from .repositories.base import HacsRepository
from .utils.data import HacsData
from .validate.manager import ValidationManager
@dataclass
class RemovedRepository:
"""Removed repository."""
repository: str | None = None
reason: str | None = None
link: str | None = None
removal_type: str = None # archived, not_compliant, critical, dev, broken
acknowledged: bool = False
def update_data(self, data: dict):
"""Update data of the repository."""
for key in data:
if data[key] is None:
continue
if key in (
"reason",
"link",
"removal_type",
"acknowledged",
):
self.__setattr__(key, data[key])
def to_json(self):
"""Return a JSON representation of the data."""
return {
"repository": self.repository,
"reason": self.reason,
"link": self.link,
"removal_type": self.removal_type,
"acknowledged": self.acknowledged,
}
@dataclass
class HacsConfiguration:
"""HacsConfiguration class."""
appdaemon_path: str = "appdaemon/apps/"
appdaemon: bool = False
config: dict[str, Any] = field(default_factory=dict)
config_entry: ConfigEntry | None = None
country: str = "ALL"
debug: bool = False
dev: bool = False
frontend_repo_url: str = ""
frontend_repo: str = ""
plugin_path: str = "www/community/"
python_script_path: str = "python_scripts/"
python_script: bool = False
release_limit: int = 5
sidepanel_icon: str = "hacs:hacs"
sidepanel_title: str = "HACS"
theme_path: str = "themes/"
theme: bool = False
token: str = None
def to_json(self) -> str:
"""Return a json string."""
return asdict(self)
def update_from_dict(self, data: dict) -> None:
"""Set attributes from dicts."""
if not isinstance(data, dict):
raise HacsException("Configuration is not valid.")
for key in data:
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
continue
self.__setattr__(key, data[key])
@dataclass
class HacsCore:
"""HACS Core info."""
config_path: pathlib.Path | None = None
ha_version: AwesomeVersion | None = None
lovelace_mode = LovelaceMode("yaml")
@dataclass
class HacsCommon:
"""Common for HACS."""
categories: set[str] = field(default_factory=set)
renamed_repositories: dict[str, str] = field(default_factory=dict)
archived_repositories: set[str] = field(default_factory=set)
ignored_repositories: set[str] = field(default_factory=set)
skip: set[str] = field(default_factory=set)
@dataclass
class HacsStatus:
"""HacsStatus."""
startup: bool = True
new: bool = False
active_frontend_endpoint_plugin: bool = False
active_frontend_endpoint_theme: bool = False
inital_fetch_done: bool = False
@dataclass
class HacsSystem:
"""HACS System info."""
disabled_reason: HacsDisabledReason | None = None
running: bool = False
stage = HacsStage.SETUP
action: bool = False
generator: bool = False
@property
def disabled(self) -> bool:
"""Return if HACS is disabled."""
return self.disabled_reason is not None
@dataclass
class HacsRepositories:
"""HACS Repositories."""
_default_repositories: set[str] = field(default_factory=set)
_repositories: set[HacsRepository] = field(default_factory=set)
_repositories_by_full_name: dict[str, HacsRepository] = field(default_factory=dict)
_repositories_by_id: dict[str, HacsRepository] = field(default_factory=dict)
_removed_repositories_by_full_name: dict[str, RemovedRepository] = field(default_factory=dict)
@property
def list_all(self) -> list[HacsRepository]:
"""Return a list of repositories."""
return list(self._repositories)
@property
def list_removed(self) -> list[RemovedRepository]:
"""Return a list of removed repositories."""
return list(self._removed_repositories_by_full_name.values())
@property
def list_downloaded(self) -> list[HacsRepository]:
"""Return a list of downloaded repositories."""
return [repo for repo in self._repositories if repo.data.installed]
def category_downloaded(self, category: HacsCategory) -> bool:
"""Check if a given category has been downloaded."""
for repository in self.list_downloaded:
if repository.data.category == category:
return True
return False
def register(self, repository: HacsRepository, default: bool = False) -> None:
"""Register a repository."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if registered_repo := self._repositories_by_id.get(repo_id):
if registered_repo.data.full_name == repository.data.full_name:
return
self.unregister(registered_repo)
registered_repo.data.full_name = repository.data.full_name
registered_repo.data.new = False
repository = registered_repo
if repository not in self._repositories:
self._repositories.add(repository)
self._repositories_by_id[repo_id] = repository
self._repositories_by_full_name[repository.data.full_name_lower] = repository
if default:
self.mark_default(repository)
def unregister(self, repository: HacsRepository) -> None:
"""Unregister a repository."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if not self.is_registered(repository_id=repo_id):
return
if self.is_default(repo_id):
self._default_repositories.remove(repo_id)
if repository in self._repositories:
self._repositories.remove(repository)
self._repositories_by_id.pop(repo_id, None)
self._repositories_by_full_name.pop(repository.data.full_name_lower, None)
def mark_default(self, repository: HacsRepository) -> None:
"""Mark a repository as default."""
repo_id = str(repository.data.id)
if repo_id == "0":
return
if not self.is_registered(repository_id=repo_id):
return
self._default_repositories.add(repo_id)
def set_repository_id(self, repository: HacsRepository, repo_id: str):
"""Update a repository id."""
existing_repo_id = str(repository.data.id)
if existing_repo_id == repo_id:
return
if existing_repo_id != "0":
raise ValueError(
f"The repo id for {repository.data.full_name_lower} "
f"is already set to {existing_repo_id}"
)
repository.data.id = repo_id
self.register(repository)
def is_default(self, repository_id: str | None = None) -> bool:
"""Check if a repository is default."""
if not repository_id:
return False
return repository_id in self._default_repositories
def is_registered(
self,
repository_id: str | None = None,
repository_full_name: str | None = None,
) -> bool:
"""Check if a repository is registered."""
if repository_id is not None:
return repository_id in self._repositories_by_id
if repository_full_name is not None:
return repository_full_name in self._repositories_by_full_name
return False
def is_downloaded(
self,
repository_id: str | None = None,
repository_full_name: str | None = None,
) -> bool:
"""Check if a repository is registered."""
if repository_id is not None:
repo = self.get_by_id(repository_id)
if repository_full_name is not None:
repo = self.get_by_full_name(repository_full_name)
if repo is None:
return False
return repo.data.installed
def get_by_id(self, repository_id: str | None) -> HacsRepository | None:
"""Get repository by id."""
if not repository_id:
return None
return self._repositories_by_id.get(str(repository_id))
def get_by_full_name(self, repository_full_name: str | None) -> HacsRepository | None:
"""Get repository by full name."""
if not repository_full_name:
return None
return self._repositories_by_full_name.get(repository_full_name.lower())
def is_removed(self, repository_full_name: str) -> bool:
"""Check if a repository is removed."""
return repository_full_name in self._removed_repositories_by_full_name
def removed_repository(self, repository_full_name: str) -> RemovedRepository:
"""Get repository by full name."""
if removed := self._removed_repositories_by_full_name.get(repository_full_name):
return removed
removed = RemovedRepository(repository=repository_full_name)
self._removed_repositories_by_full_name[repository_full_name] = removed
return removed
class HacsBase:
"""Base HACS class."""
data: HacsData | None = None
data_client: HacsDataClient | None = None
frontend_version: str | None = None
github: GitHub | None = None
githubapi: GitHubAPI | None = None
hass: HomeAssistant | None = None
integration: Integration | None = None
queue: QueueManager | None = None
repository: AIOGitHubAPIRepository | None = None
session: ClientSession | None = None
stage: HacsStage | None = None
validation: ValidationManager | None = None
version: AwesomeVersion | None = None
def __init__(self) -> None:
"""Initialize."""
self.common = HacsCommon()
self.configuration = HacsConfiguration()
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
self.core = HacsCore()
self.log = LOGGER
self.recurring_tasks: list[Callable[[], None]] = []
self.repositories = HacsRepositories()
self.status = HacsStatus()
self.system = HacsSystem()
@property
def integration_dir(self) -> pathlib.Path:
"""Return the HACS integration dir."""
return self.integration.file_path
def set_stage(self, stage: HacsStage | None) -> None:
"""Set HACS stage."""
if stage and self.stage == stage:
return
self.stage = stage
if stage is not None:
self.log.info("Stage changed: %s", self.stage)
self.async_dispatch(HacsDispatchEvent.STAGE, {"stage": self.stage})
def disable_hacs(self, reason: HacsDisabledReason) -> None:
"""Disable HACS."""
if self.system.disabled_reason == reason:
return
self.system.disabled_reason = reason
if reason != HacsDisabledReason.REMOVED:
self.log.error("HACS is disabled - %s", reason)
if reason == HacsDisabledReason.INVALID_TOKEN:
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
def enable_hacs(self) -> None:
"""Enable HACS."""
if self.system.disabled_reason is not None:
self.system.disabled_reason = None
self.log.info("HACS is enabled")
def enable_hacs_category(self, category: HacsCategory) -> None:
"""Enable HACS category."""
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()
def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
self.coordinators.pop(category)
async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file."""
def _write_file():
with open(
file_path,
mode="w" if isinstance(content, str) else "wb",
encoding="utf-8" if isinstance(content, str) else None,
errors="ignore" if isinstance(content, str) else None,
) as file_handler:
file_handler.write(content)
# Create gz for .js files
if os.path.isfile(file_path):
if file_path.endswith(".js"):
with open(file_path, "rb") as f_in:
with gzip.open(file_path + ".gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
# LEGACY! Remove with 2.0
if "themes" in file_path and file_path.endswith(".yaml"):
filename = file_path.split("/")[-1]
base = file_path.split("/themes/")[0]
combined = f"{base}/themes/{filename}"
if os.path.exists(combined):
self.log.info("Removing old theme file %s", combined)
os.remove(combined)
try:
await self.hass.async_add_executor_job(_write_file)
except (
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as error:
self.log.error("Could not write data to %s - %s", file_path, error)
return False
return await async_exists(self.hass, file_path)
async def async_can_update(self) -> int:
"""Helper to calculate the number of repositories we can fetch data for."""
try:
response = await self.async_github_api_method(self.githubapi.rate_limit)
if ((limit := response.data.resources.core.remaining or 0) - 1000) >= 10:
return math.floor((limit - 1000) / 10)
reset = dt.as_local(dt.utc_from_timestamp(response.data.resources.core.reset))
self.log.info(
"GitHub API ratelimited - %s remaining (%s)",
response.data.resources.core.remaining,
f"{reset.hour}:{reset.minute}:{reset.second}",
)
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
except (
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
return 0
async def async_github_api_method(
self,
method: Callable[[], Awaitable[TV]],
*args,
raise_exception: bool = True,
**kwargs,
) -> TV | None:
"""Call a GitHub API method"""
_exception = None
try:
return await method(*args, **kwargs)
except GitHubAuthenticationException as exception:
self.disable_hacs(HacsDisabledReason.INVALID_TOKEN)
_exception = exception
except GitHubRatelimitException as exception:
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
_exception = exception
except GitHubNotModifiedException as exception:
raise exception
except GitHubException as exception:
_exception = exception
except (
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.log.exception(exception)
_exception = exception
if raise_exception and _exception is not None:
raise HacsException(_exception)
return None
async def async_register_repository(
self,
repository_full_name: str,
category: HacsCategory,
*,
check: bool = True,
ref: str | None = None,
repository_id: str | None = None,
default: bool = False,
) -> None:
"""Register a repository."""
if repository_full_name in self.common.skip:
if repository_full_name != HacsGitHubRepo.INTEGRATION:
raise HacsExpectedException(f"Skipping {repository_full_name}")
if repository_full_name == "home-assistant/core":
raise HomeAssistantCoreRepositoryException()
if repository_full_name == "home-assistant/addons" or repository_full_name.startswith(
"hassio-addons/"
):
raise AddonRepositoryException()
if category not in REPOSITORY_CLASSES:
self.log.warning(
"%s is not a valid repository category, %s will not be registered.",
category,
repository_full_name,
)
return
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
repository_full_name = renamed
repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
if check:
try:
await repository.async_registration(ref)
if repository.validate.errors:
self.common.skip.add(repository.data.full_name)
if not self.status.startup:
self.log.error("Validation for %s failed.", repository_full_name)
if self.system.action:
raise HacsException(
f"::error:: Validation for {repository_full_name} failed."
)
return repository.validate.errors
if self.system.action:
repository.logger.info("%s Validation completed", repository.string)
else:
repository.logger.info("%s Registration completed", repository.string)
except (HacsRepositoryExistException, HacsRepositoryArchivedException) as exception:
if self.system.generator:
repository.logger.error(
"%s Registration Failed - %s", repository.string, exception
)
return
except AIOGitHubAPIException as exception:
self.common.skip.add(repository.data.full_name)
raise HacsException(
f"Validation for {repository_full_name} failed with {exception}."
) from exception
if self.status.new:
repository.data.new = False
if repository_id is not None:
repository.data.id = repository_id
else:
if self.hass is not None and check and repository.data.new:
self.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"action": "registration",
"repository": repository.data.full_name,
"repository_id": repository.data.id,
},
)
self.repositories.register(repository, default)
async def startup_tasks(self, _=None) -> None:
"""Tasks that are started after setup."""
self.set_stage(HacsStage.STARTUP)
await self.async_load_hacs_from_github()
if critical := await async_load_from_store(self.hass, "critical"):
for repo in critical:
if not repo["acknowledged"]:
self.log.critical("URGENT!: Check the HACS panel!")
async_create_persistent_notification(
self.hass, title="URGENT!", message="**Check the HACS panel!**"
)
break
self.recurring_tasks.append(
async_track_time_interval(
self.hass,
self.async_load_hacs_from_github,
timedelta(hours=48),
)
)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
)
)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_get_all_category_repositories, timedelta(hours=6)
)
)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
)
self.recurring_tasks.append(
async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10))
)
self.recurring_tasks.append(
async_track_time_interval(
self.hass, self.async_handle_critical_repositories, timedelta(hours=6)
)
)
unsub = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
)
if config_entry := self.configuration.config_entry:
config_entry.async_on_unload(unsub)
self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks))
self.status.startup = False
self.async_dispatch(HacsDispatchEvent.STATUS, {})
await self.async_handle_removed_repositories()
await self.async_get_all_category_repositories()
self.set_stage(HacsStage.RUNNING)
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
await self.async_handle_critical_repositories()
await self.async_process_queue()
self.async_dispatch(HacsDispatchEvent.STATUS, {})
async def async_download_file(
self,
url: str,
*,
headers: dict | None = None,
keep_url: bool = False,
nolog: bool = False,
handle_rate_limit: bool = False,
**_,
) -> bytes | None:
"""Download files, and return the content."""
if url is None:
return None
if not keep_url and "tags/" in url:
url = url.replace("tags/", "")
self.log.debug("Trying to download %s", url)
attempt_count = 0
while attempt_count < 5:
try:
request = await self.session.get(
url=url,
timeout=ClientTimeout(total=60),
headers=headers,
)
# Make sure that we got a valid result
if request.status == 200:
return await request.read()
# Handle rate-limits
if handle_rate_limit and request.status == 429:
header = int(request.headers.get("retry-after") or 10)
retry_after = min(header, 60) # Limit to 60 seconds
self.log.warning(
"GitHub has imposed a ratelimit on the request for %s, "
"retrying after %s seconds",
url,
retry_after,
)
attempt_count += 1
await asyncio.sleep(retry_after)
continue
raise HacsException(
f"Got status code {request.status} when trying to download {url}"
)
except TimeoutError:
self.log.warning(
"A timeout of 60! seconds was encountered while downloading %s, "
"using over 60 seconds to download a single file is not normal. "
"This is not a problem with HACS but how your host communicates with GitHub. "
"Retrying up to 5 times to mask/hide your host/network problems to "
"stop the flow of issues opened about it. "
"Tries left %s",
url,
(4 - attempt_count),
)
attempt_count += 1
await asyncio.sleep(1)
continue
except (
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
if not nolog:
self.log.exception("Download failed - %s", exception)
return None
async def async_recreate_entities(self) -> None:
"""Recreate entities."""
platforms = [Platform.UPDATE]
# Workaround for core versions without https://github.com/home-assistant/core/pull/117084
if self.core.ha_version < AwesomeVersion("2024.6.0"):
unload_platforms_lock = asyncio.Lock()
async with unload_platforms_lock:
on_unload = self.configuration.config_entry._on_unload
self.configuration.config_entry._on_unload = []
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
self.configuration.config_entry._on_unload = on_unload
else:
await self.hass.config_entries.async_unload_platforms(
entry=self.configuration.config_entry,
platforms=platforms,
)
await self.hass.config_entries.async_forward_entry_setups(
self.configuration.config_entry, platforms
)
@callback
def async_dispatch(self, signal: HacsDispatchEvent, data: dict | None = None) -> None:
"""Dispatch a signal with data."""
async_dispatcher_send(self.hass, signal, data)
def set_active_categories(self) -> None:
"""Set the active categories."""
self.common.categories = set()
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE):
self.enable_hacs_category(HacsCategory(category))
if (
HacsCategory.PYTHON_SCRIPT in self.hass.config.components
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
):
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
if self.hass.services.has_service(
"frontend", "reload_themes"
) or self.repositories.category_downloaded(HacsCategory.THEME):
self.enable_hacs_category(HacsCategory.THEME)
if self.configuration.appdaemon:
self.enable_hacs_category(HacsCategory.APPDAEMON)
async def async_load_hacs_from_github(self, _=None) -> None:
"""Load HACS from GitHub."""
if self.status.inital_fetch_done:
return
try:
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
should_recreate_entities = False
if repository is None:
should_recreate_entities = True
await self.async_register_repository(
repository_full_name=HacsGitHubRepo.INTEGRATION,
category=HacsCategory.INTEGRATION,
default=True,
)
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
elif not self.status.startup:
self.log.error("Scheduling update of hacs/integration")
self.queue.add(repository.common_update())
if repository is None:
raise HacsException("Unknown error")
repository.data.installed = True
repository.data.installed_version = self.integration.version.string
repository.data.new = False
repository.data.releases = True
if should_recreate_entities:
await self.async_recreate_entities()
self.repository = repository.repository_object
self.repositories.mark_default(repository)
except HacsException as exception:
if "403" in str(exception):
self.log.critical(
"GitHub API is ratelimited, or the token is wrong.",
)
else:
self.log.critical("Could not load HACS! - %s", exception)
self.disable_hacs(HacsDisabledReason.LOAD_HACS)
async def async_get_all_category_repositories(self, _=None) -> None:
"""Get all category repositories."""
if self.system.disabled:
return
self.log.info("Loading known repositories")
await asyncio.gather(
*[
self.async_get_category_repositories_experimental(category)
for category in self.common.categories or []
]
)
async def async_get_category_repositories_experimental(self, category: str) -> None:
"""Update all category repositories."""
self.log.debug("Fetching updated content for %s", category)
try:
category_data = await self.data_client.get_data(category, validate=True)
except HacsNotModifiedException:
self.log.debug("No updates for %s", category)
return
except HacsException as exception:
self.log.error("Could not update %s - %s", category, exception)
return
await self.data.register_unknown_repositories(category_data, category)
for repo_id, repo_data in category_data.items():
repo_name = repo_data["full_name"]
if self.common.renamed_repositories.get(repo_name):
repo_name = self.common.renamed_repositories[repo_name]
if self.repositories.is_removed(repo_name):
continue
if repo_name in self.common.archived_repositories:
continue
if repository := self.repositories.get_by_full_name(repo_name):
self.repositories.set_repository_id(repository, repo_id)
self.repositories.mark_default(repository)
if repository.data.last_fetched is None or (
repository.data.last_fetched.timestamp() < repo_data["last_fetched"]
):
repository.data.update_data({**dict(REPOSITORY_KEYS_TO_EXPORT), **repo_data})
if (manifest := repo_data.get("manifest")) is not None:
repository.repository_manifest.update_data(
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest}
)
if category == "integration":
self.status.inital_fetch_done = True
if self.stage == HacsStage.STARTUP:
for repository in self.repositories.list_all:
if (
repository.data.category == category
and not repository.data.installed
and not self.repositories.is_default(repository.data.id)
):
repository.logger.debug(
"%s Unregister stale custom repository", repository.string
)
self.repositories.unregister(repository)
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
self.coordinators[category].async_update_listeners()
async def async_check_rate_limit(self, _=None) -> None:
"""Check rate limit."""
if not self.system.disabled or self.system.disabled_reason != HacsDisabledReason.RATE_LIMIT:
return
self.log.debug("Checking if ratelimit has lifted")
can_update = await self.async_can_update()
self.log.debug("Ratelimit indicate we can update %s", can_update)
if can_update > 0:
self.enable_hacs()
await self.async_process_queue()
async def async_process_queue(self, _=None) -> None:
"""Process the queue."""
if self.system.disabled:
self.log.debug("HACS is disabled")
return
if not self.queue.has_pending_tasks:
self.log.debug("Nothing in the queue")
return
if self.queue.running:
self.log.debug("Queue is already running")
return
async def _handle_queue():
if not self.queue.has_pending_tasks:
await self.data.async_write()
return
can_update = await self.async_can_update()
self.log.debug(
"Can update %s repositories, items in queue %s",
can_update,
self.queue.pending_tasks,
)
if can_update != 0:
try:
await self.queue.execute(can_update)
except HacsExecutionStillInProgress:
return
await _handle_queue()
await _handle_queue()
async def async_handle_removed_repositories(self, _=None) -> None:
"""Handle removed repositories."""
if self.system.disabled:
return
need_to_save = False
self.log.info("Loading removed repositories")
try:
removed_repositories = await self.data_client.get_data("removed", validate=True)
except HacsException:
return
for item in removed_repositories:
removed = self.repositories.removed_repository(item["repository"])
removed.update_data(item)
for removed in self.repositories.list_removed:
if (repository := self.repositories.get_by_full_name(removed.repository)) is None:
continue
if repository.data.full_name in self.common.ignored_repositories:
continue
if repository.data.installed:
if removed.removal_type != "critical":
async_create_issue(
hass=self.hass,
domain=DOMAIN,
issue_id=f"removed_{repository.data.id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="removed",
translation_placeholders={
"name": repository.data.full_name,
"reason": removed.reason,
"repositry_id": repository.data.id,
},
)
self.log.warning(
"You have '%s' installed with HACS "
"this repository has been removed from HACS, please consider removing it. "
"Removal reason (%s)",
repository.data.full_name,
removed.reason,
)
else:
need_to_save = True
repository.remove()
if need_to_save:
await self.data.async_write()
async def async_update_downloaded_custom_repositories(self, _=None) -> None:
"""Execute the task."""
if self.system.disabled:
return
self.log.info("Starting recurring background task for downloaded custom repositories")
repositories_to_update = 0
repositories_updated = asyncio.Event()
async def update_repository(repository: HacsRepository) -> None:
"""Update a repository"""
nonlocal repositories_to_update
await repository.update_repository(ignore_issues=True)
repositories_to_update -= 1
if not repositories_to_update:
repositories_updated.set()
for repository in self.repositories.list_downloaded:
if (
repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id)
):
repositories_to_update += 1
self.queue.add(update_repository(repository))
async def update_coordinators() -> None:
"""Update all coordinators."""
await repositories_updated.wait()
for coordinator in self.coordinators.values():
coordinator.async_update_listeners()
if config_entry := self.configuration.config_entry:
config_entry.async_create_background_task(
self.hass, update_coordinators(), "update_coordinators"
)
else:
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")
self.log.debug("Recurring background task for downloaded custom repositories done")
async def async_handle_critical_repositories(self, _=None) -> None:
"""Handle critical repositories."""
critical_queue = QueueManager(hass=self.hass)
instored = []
critical = []
was_installed = False
try:
critical = await self.data_client.get_data("critical", validate=True)
except (GitHubNotModifiedException, HacsNotModifiedException):
return
except HacsException:
pass
if not critical:
self.log.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 = self.repositories.removed_repository(repository["repository"])
removed_repo.removal_type = "critical"
repo = self.repositories.get_by_full_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.data.installed:
self.log.critical(
"Removing repository %s, it is marked as critical",
repository["repository"],
)
was_installed = True
stored["acknowledged"] = False
# Remove from HACS
critical_queue.add(repo.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.log.critical("Restarting Home Assistant")
self.hass.async_create_task(self.hass.async_stop(100))
async def async_setup_frontend_endpoint_plugin(self) -> None:
"""Setup the http endpoints for plugins if its not already handled."""
if self.status.active_frontend_endpoint_plugin or not await async_exists(
self.hass, self.hass.config.path("www/community")
):
return
self.log.info("Setting up plugin endpoint")
use_cache = self.core.lovelace_mode == "storage"
self.log.info(
"<HacsFrontend> %s mode, cache for /hacsfiles/: %s",
self.core.lovelace_mode,
use_cache,
)
await async_register_static_path(
self.hass,
URL_BASE,
self.hass.config.path("www/community"),
cache_headers=use_cache,
)
self.status.active_frontend_endpoint_plugin = True
================================================
FILE: custom_components/hacs/config_flow.py
================================================
"""Adds config flow for HACS."""
from __future__ import annotations
import asyncio
from contextlib import suppress
from typing import TYPE_CHECKING
from aiogithubapi import (
GitHubDeviceAPI,
GitHubException,
GitHubLoginDeviceModel,
GitHubLoginOauthModel,
)
from aiogithubapi.common.const import OAUTH_USER_LOGIN
from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigFlow, OptionsFlow
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.helpers import aiohttp_client
from homeassistant.loader import async_get_integration
import voluptuous as vol
from .base import HacsBase
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
from .utils.configuration_schema import (
APPDAEMON,
COUNTRY,
SIDEPANEL_ICON,
SIDEPANEL_TITLE,
)
from .utils.logger import LOGGER
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for HACS."""
VERSION = 1
hass: HomeAssistant
activation_task: asyncio.Task | None = None
device: GitHubDeviceAPI | None = None
_registration: GitHubLoginDeviceModel | None = None
_activation: GitHubLoginOauthModel | None = None
_reauth: bool = False
def __init__(self) -> None:
"""Initialize."""
self._errors = {}
self._user_input = {}
async def async_step_user(self, user_input):
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input:
if [x for x in user_input if x.startswith("acc_") and not user_input[x]]:
self._errors["base"] = "acc"
return await self._show_config_form(user_input)
self._user_input = user_input
return await self.async_step_device(user_input)
# Initial form
return await self._show_config_form(user_input)
async def async_step_device(self, _user_input):
"""Handle device steps."""
async def _wait_for_activation() -> None:
try:
response = await self.device.activation(device_code=self._registration.device_code)
self._activation = response.data
finally:
async def _progress():
with suppress(UnknownFlow):
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
if not self.device:
integration = await async_get_integration(self.hass, DOMAIN)
self.device = GitHubDeviceAPI(
client_id=CLIENT_ID,
session=aiohttp_client.async_get_clientsession(self.hass),
**{"client_name": f"HACS/{integration.version}"},
)
try:
response = await self.device.register()
self._registration = response.data
except GitHubException as exception:
LOGGER.exception(exception)
return self.async_abort(reason="could_not_register")
if self.activation_task is None:
self.activation_task = self.hass.async_create_task(_wait_for_activation())
if self.activation_task.done():
if (exception := self.activation_task.exception()) is not None:
LOGGER.exception(exception)
return self.async_show_progress_done(next_step_id="could_not_register")
return self.async_show_progress_done(next_step_id="device_done")
show_progress_kwargs = {
"step_id": "device",
"progress_action": "wait_for_device",
"description_placeholders": {
"url": OAUTH_USER_LOGIN,
"code": self._registration.user_code,
},
"progress_task": self.activation_task,
}
return self.async_show_progress(**show_progress_kwargs)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
if not user_input:
user_input = {}
if AwesomeVersion(HAVERSION) < MINIMUM_HA_VERSION:
return self.async_abort(
reason="min_ha_version",
description_placeholders={"version": MINIMUM_HA_VERSION},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("acc_logs", default=user_input.get("acc_logs", False)): bool,
vol.Required("acc_addons", default=user_input.get("acc_addons", False)): bool,
vol.Required(
"acc_untested", default=user_input.get("acc_untested", False)
): bool,
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
}
),
errors=self._errors,
)
async def async_step_device_done(self, user_input: dict[str, bool] | None = None):
"""Handle device steps"""
if self._reauth:
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self.hass.config_entries.async_update_entry(
existing_entry, data={**existing_entry.data, "token": self._activation.access_token}
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title="",
data={
"token": self._activation.access_token,
},
options={
"experimental": True,
},
)
async def async_step_could_not_register(self, _user_input=None):
"""Handle issues that need transition await from progress step."""
return self.async_abort(reason="could_not_register")
async def async_step_reauth(self, _user_input=None):
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({}),
)
self._reauth = True
return await self.async_step_device(None)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HacsOptionsFlowHandler(config_entry)
class HacsOptionsFlowHandler(OptionsFlow):
"""HACS config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
if AwesomeVersion(HAVERSION) < "2024.11.99":
self.config_entry = config_entry
async def async_step_init(self, _user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
hacs: HacsBase = self.hass.data.get(DOMAIN)
if user_input is not None:
return self.async_create_entry(title="", data={**user_input, "experimental": True})
if hacs is None or hacs.configuration is None:
return self.async_abort(reason="not_setup")
if hacs.queue.has_pending_tasks:
return self.async_abort(reason="pending_tasks")
schema = {
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
}
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))
================================================
FILE: custom_components/hacs/const.py
================================================
"""Constants for HACS"""
from typing import TypeVar
from aiogithubapi.common.const import ACCEPT_HEADERS
NAME_SHORT = "HACS"
DOMAIN = "hacs"
CLIENT_ID = "395a8e669c5de9f7c6e8"
MINIMUM_HA_VERSION = "0.0.0"
URL_BASE = "/hacsfiles"
TV = TypeVar("TV")
PACKAGE_NAME = "custom_components.hacs"
DEFAULT_CONCURRENT_TASKS = 15
DEFAULT_CONCURRENT_BACKOFF_TIME = 1
HACS_REPOSITORY_ID = "172733314"
HACS_ACTION_GITHUB_API_HEADERS = {
"User-Agent": "HACS/action",
"Accept": ACCEPT_HEADERS["preview"],
}
VERSION_STORAGE = "6"
STORENAME = "hacs"
HACS_SYSTEM_ID = "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd"
LOCALE = [
"ALL",
"AF",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"KH",
"CM",
"CA",
"CV",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"HR",
"CU",
"CW",
"CY",
"CZ",
"CI",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KP",
"KR",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MK",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RO",
"RU",
"RW",
"RE",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SZ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"US",
"UM",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW",
]
================================================
FILE: custom_components/hacs/coordinator.py
================================================
"""Coordinator to trigger entity updates."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol
class HacsUpdateCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Dispatch updates to update entities."""
def __init__(self) -> None:
"""Initialize."""
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""
@callback
def remove_listener() -> None:
"""Remove update listener."""
self._listeners.pop(remove_listener)
self._listeners[remove_listener] = (update_callback, context)
return remove_listener
@callback
def async_update_listeners(self) -> None:
"""Update all registered listeners."""
for update_callback, _ in list(self._listeners.values()):
update_callback()
================================================
FILE: custom_components/hacs/data_client.py
================================================
"""HACS Data client."""
from __future__ import annotations
import asyncio
from typing import Any
from aiohttp import ClientSession, ClientTimeout
import voluptuous as vol
from .exceptions import HacsException, HacsNotModifiedException
from .utils.logger import LOGGER
from .utils.validate import (
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
VALIDATE_FETCHED_V2_REPO_DATA,
)
CRITICAL_REMOVED_VALIDATORS = {
"critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
"removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
}
class HacsDataClient:
"""HACS Data client."""
def __init__(self, session: ClientSession, client_name: str) -> None:
"""Initialize."""
self._client_name = client_name
self._etags = {}
self._session = session
async def _do_request(
self,
filename: str,
section: str | None = None,
) -> dict[str, dict[str, Any]] | list[str]:
"""Do request."""
endpoint = "/".join([v for v in [section, filename] if v is not None])
try:
response = await self._session.get(
f"https://data-v2.hacs.xyz/{endpoint}",
timeout=ClientTimeout(total=60),
headers={
"User-Agent": self._client_name,
"If-None-Match": self._etags.get(endpoint, ""),
},
)
if response.status == 304:
raise HacsNotModifiedException() from None
response.raise_for_status()
except HacsNotModifiedException:
raise
except TimeoutError:
raise HacsException("Timeout of 60s reached") from None
except Exception as exception:
raise HacsException(f"Error fetching data from HACS: {exception}") from exception
self._etags[endpoint] = response.headers.get("etag")
return await response.json()
async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]:
"""Get data."""
data = await self._do_request(filename="data.json", section=section)
if not validate:
return data
if section in VALIDATE_FETCHED_V2_REPO_DATA:
validated = {}
for key, repo_data in data.items():
try:
validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data)
except vol.Invalid as exception:
LOGGER.info(
"Got invalid data for %s (%s)", repo_data.get("full_name", key), exception
)
continue
return validated
if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)):
raise ValueError(f"Do not know how to validate {section}")
validated = []
for repo_data in data:
try:
validated.append(validator(repo_data))
except vol.Invalid as exception:
LOGGER.info("Got invalid data for %s (%s)", section, exception)
continue
return validated
async def get_repositories(self, section: str) -> list[str]:
"""Get repositories."""
return await self._do_request(filename="repositories.json", section=section)
================================================
FILE: custom_components/hacs/diagnostics.py
================================================
"""Diagnostics support for HACS."""
from __future__ import annotations
from typing import Any
from aiogithubapi import GitHubException
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .base import HacsBase
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
hacs: HacsBase = hass.data[DOMAIN]
data = {
"entry": entry.as_dict(),
"hacs": {
"stage": hacs.stage,
"version": hacs.version,
"disabled_reason": hacs.system.disabled_reason,
"new": hacs.status.new,
"startup": hacs.status.startup,
"categories": hacs.common.categories,
"renamed_repositories": hacs.common.renamed_repositories,
"archived_repositories": hacs.common.archived_repositories,
"ignored_repositories": hacs.common.ignored_repositories,
"lovelace_mode": hacs.core.lovelace_mode,
"configuration": {},
},
"custom_repositories": [
repo.data.full_name
for repo in hacs.repositories.list_all
if not hacs.repositories.is_default(str(repo.data.id))
],
"repositories": [],
}
for key in (
"appdaemon",
"country",
"debug",
"dev",
"python_script",
"release_limit",
"theme",
):
data["hacs"]["configuration"][key] = getattr(hacs.configuration, key, None)
for repository in hacs.repositories.list_downloaded:
data["repositories"].append(
{
"data": repository.data.to_json(),
"integration_manifest": repository.integration_manifest,
"repository_manifest": repository.repository_manifest.to_dict(),
"ref": repository.ref,
"paths": {
"localpath": repository.localpath.replace(hacs.core.config_path, "/config"),
"local": repository.content.path.local.replace(
hacs.core.config_path, "/config"
),
"remote": repository.content.path.remote,
},
}
)
try:
rate_limit_response = await hacs.githubapi.rate_limit()
data["rate_limit"] = rate_limit_response.data.as_dict
except GitHubException as exception:
data["rate_limit"] = str(exception)
return async_redact_data(data, ("token",))
================================================
FILE: custom_components/hacs/entity.py
================================================
"""HACS Base entities."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo
if TYPE_CHECKING:
from .base import HacsBase
from .repositories.base import HacsRepository
def system_info(hacs: HacsBase) -> dict:
"""Return system info."""
return {
"identifiers": {(DOMAIN, HACS_SYSTEM_ID)},
"name": NAME_SHORT,
"manufacturer": "hacs.xyz",
"model": "",
"sw_version": str(hacs.version),
"configuration_url": "homeassistant://hacs",
"entry_type": DeviceEntryType.SERVICE,
}
class HacsBaseEntity(Entity):
"""Base HACS entity."""
repository: HacsRepository | None = None
_attr_should_poll = False
def __init__(self, hacs: HacsBase) -> None:
"""Initialize."""
self.hacs = hacs
class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""
async def async_added_to_hass(self) -> None:
"""Register for status events."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
HacsDispatchEvent.REPOSITORY,
self._update_and_write_state,
)
)
@callback
def _update(self) -> None:
"""Update the sensor."""
async def async_update(self) -> None:
"""Manual updates of the sensor."""
self._update()
@callback
def _update_and_write_state(self, _: Any) -> None:
"""Update the entity and write state."""
self._update()
self.async_write_ha_state()
class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity."""
_attr_icon = "hacs:hacs"
_attr_unique_id = HACS_SYSTEM_ID
@property
def device_info(self) -> dict[str, any]:
"""Return device information about HACS."""
return system_info(self.hacs)
class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity."""
def __init__(
self,
hacs: HacsBase,
repository: HacsRepository,
) -> None:
"""Initialize."""
BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository
self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.hacs.repositories.is_downloaded(repository_id=str(self.repository.data.id))
@property
def device_info(self) -> dict[str, any]:
"""Return device information about HACS."""
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
return system_info(self.hacs)
def _manufacturer():
if authors := self.repository.data.authors:
return ", ".join(author.replace("@", "") for author in authors)
return self.repository.data.full_name.split("/")[0]
return {
"identifiers": {(DOMAIN, str(self.repository.data.id))},
"name": self.repository.display_name,
"model": self.repository.data.category,
"manufacturer": _manufacturer(),
"configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
"entry_type": DeviceEntryType.SERVICE,
}
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._repo_last_fetched is not None
and self.repository.data.last_fetched is not None
and self._repo_last_fetched >= self.repository.data.last_fetched
):
return
self._repo_last_fetched = self.repository.data.last_fetched
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
================================================
FILE: custom_components/hacs/enums.py
================================================
"""Helper constants."""
# pylint: disable=missing-class-docstring
from enum import StrEnum
class HacsGitHubRepo(StrEnum):
"""HacsGitHubRepo."""
DEFAULT = "hacs/default"
INTEGRATION = "hacs/integration"
class HacsCategory(StrEnum):
APPDAEMON = "appdaemon"
INTEGRATION = "integration"
LOVELACE = "lovelace"
PLUGIN = "plugin" # Kept for legacy purposes
PYTHON_SCRIPT = "python_script"
TEMPLATE = "template"
THEME = "theme"
REMOVED = "removed"
def __str__(self):
return str(self.value)
class HacsDispatchEvent(StrEnum):
"""HacsDispatchEvent."""
CONFIG = "hacs_dispatch_config"
ERROR = "hacs_dispatch_error"
RELOAD = "hacs_dispatch_reload"
REPOSITORY = "hacs_dispatch_repository"
REPOSITORY_DOWNLOAD_PROGRESS = "hacs_dispatch_repository_download_progress"
STAGE = "hacs_dispatch_stage"
STARTUP = "hacs_dispatch_startup"
STATUS = "hacs_dispatch_status"
class RepositoryFile(StrEnum):
"""Repository file names."""
HACS_JSON = "hacs.json"
MAINIFEST_JSON = "manifest.json"
class LovelaceMode(StrEnum):
"""Lovelace Modes."""
STORAGE = "storage"
AUTO = "auto"
AUTO_GEN = "auto-gen"
YAML = "yaml"
class HacsStage(StrEnum):
SETUP = "setup"
STARTUP = "startup"
WAITING = "waiting"
RUNNING = "running"
BACKGROUND = "background"
class HacsDisabledReason(StrEnum):
RATE_LIMIT = "rate_limit"
REMOVED = "removed"
INVALID_TOKEN = "invalid_token"
CONSTRAINS = "constrains"
LOAD_HACS = "load_hacs"
RESTORE = "restore"
================================================
FILE: custom_components/hacs/exceptions.py
================================================
"""Custom Exceptions for HACS."""
class HacsException(Exception):
"""Super basic."""
class HacsRepositoryArchivedException(HacsException):
"""For repositories that are archived."""
class HacsNotModifiedException(HacsException):
"""For responses that are not modified."""
class HacsExpectedException(HacsException):
"""For stuff that are expected."""
class HacsRepositoryExistException(HacsException):
"""For repositories that are already exist."""
class HacsExecutionStillInProgress(HacsException):
"""Exception to raise if execution is still in progress."""
class AddonRepositoryException(HacsException):
"""Exception to raise when user tries to add add-on repository."""
exception_message = (
"The repository does not seem to be a integration, "
"but an add-on repository. HACS does not manage add-ons."
)
def __init__(self) -> None:
super().__init__(self.exception_message)
class HomeAssistantCoreRepositoryException(HacsException):
"""Exception to raise when user tries to add the home-assistant/core repository."""
exception_message = (
"You can not add homeassistant/core, to use core integrations "
"check the Home Assistant documentation for how to add them."
)
def __init__(self) -> None:
super().__init__(self.exception_message)
================================================
FILE: custom_components/hacs/frontend.py
================================================
"""Starting setup task: Frontend."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from homeassistant.components.frontend import (
add_extra_js_url,
async_register_built_in_panel,
)
from .const import DOMAIN, URL_BASE
from .hacs_frontend import VERSION as FE_VERSION, locate_dir
from .utils.workarounds import async_register_static_path
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .base import HacsBase
async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
"""Register the frontend."""
# Register frontend
if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")):
hacs.log.warning(
"<HacsFrontend> Frontend development mode enabled. Do not run in production!"
)
await async_register_static_path(
hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
)
hacs.frontend_version = "dev"
else:
await async_register_static_path(
hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
)
hacs.frontend_version = FE_VERSION
# Custom iconset
await async_register_static_path(
hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
)
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
# Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}):
async_register_built_in_panel(
hass,
component_name="custom",
sidebar_title=hacs.configuration.sidepanel_title,
sidebar_icon=hacs.configuration.sidepanel_icon,
frontend_url_path=DOMAIN,
config={
"_panel_custom": {
"name": "hacs-frontend",
"embed_iframe": True,
"trust_external": False,
"js_url": f"/hacsfiles/frontend/entrypoint.js?hacstag={hacs.frontend_version}",
}
},
require_admin=True,
)
# Setup plugin endpoint if needed
await hacs.async_setup_frontend_endpoint_plugin()
================================================
FILE: custom_components/hacs/icons.json
================================================
{
"entity": {
"switch": {
"pre-release": {
"state": {
"on": "mdi:test-tube",
"off": "mdi:test-tube-off"
}
}
}
}
}
================================================
FILE: custom_components/hacs/iconset.js
================================================
const hacsIcons = {
hacs: {
path: "m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 -0.280572,0.153036 -0.605773,0.229553 -0.950094,0.229553 -0.0765,0 -0.146661,-0.0064 -0.216801,-0.01275 -0.605774,-0.05739 -1.135016,-0.344329 -1.402827,-0.7588 l 0.784304,-0.516495 c 0.0893,0.146659 0.344331,0.312448 0.707793,0.34433 0.235931,0.02551 0.471852,-0.01913 0.637643,-0.108401 0.101998,-0.05101 0.172171,-0.127529 0.17854,-0.191295 0.0065,-0.08289 -0.0255,-0.369835 -0.733293,-0.439975 -1.013854,-0.09565 -1.645127,-0.688661 -1.568606,-1.460214 0.0319,-0.382589 0.280561,-0.714165 0.663153,-0.930965 0.331571,-0.172165 0.752423,-0.25506 1.166895,-0.210424 0.599382,0.05739 1.128635,0.344329 1.402816,0.7588 l -0.784304,0.510118 c -0.0893,-0.140282 -0.344331,-0.299694 -0.707782,-0.331576 -0.235932,-0.02551 -0.471863,0.01913 -0.637654,0.10202 -0.0956,0.05739 -0.165791,0.133906 -0.17216,0.191295 -0.0255,0.293317 0.465482,0.420847 0.726913,0.439976 v 0.0064 c 1.020234,0.09565 1.638757,0.66953 1.562237,1.460213 z m -7.466854,-0.988354 c 0,-1.192401 0.962855,-2.155249 2.15525,-2.155249 0.599393,0 1.179645,0.25506 1.594117,0.707789 l -0.695033,0.624895 c -0.235931,-0.25506 -0.561133,-0.401718 -0.899084,-0.401718 -0.675903,0 -1.217906,0.542 -1.217906,1.217906 0,0.66953 0.542003,1.217908 1.217906,1.217908 0.337951,0 0.663153,-0.140283 0.899084,-0.401718 l 0.695033,0.631271 c -0.414472,0.452729 -0.988355,0.707788 -1.594117,0.707788 -1.192395,0 -2.15525,-0.969224 -2.15525,-2.148872 z M 8.6573365,23.461054 10.353474,19.14418 h 0.624893 l 1.568618,4.316874 H 11.52037 L 11.265308,22.734136 H 9.964513 l -0.274192,0.726918 z m 1.6833885,-1.68339 h 0.580263 L 10.646796,21.012487 Z M 8.1089536,19.156932 v 4.297745 H 7.1461095 v -1.645131 h -1.606867 v 1.645131 H 4.5763876 v -4.297745 h 0.9628549 v 1.696143 h 1.606867 V 19.156932 Z M 20.115859,4.2997436 C 20.090359,4.159461 19.969198,4.0574375 19.822548,4.0574375 H 14.141102 10.506516 4.8250686 c -0.14665,0 -0.2678112,0.1020202 -0.2933108,0.2423061 L 3.690064,8.8461703 c -0.00651,0.01913 -0.00651,0.03826 -0.00651,0.057391 v 1.5239797 c 0,0.165789 0.133911,0.299694 0.2996911,0.299694 H 4.5762579 20.0711 20.664112 c 0.165781,0 0.299691,-0.133905 0.299691,-0.299694 V 8.8971848 c 0,-0.01913 0,-0.03826 -0.0065,-0.05739 z M 4.5763876,17.358767 c 0,0.184917 0.1466608,0.331577 0.3315819,0.331577 h 5.5985465 3.634586 0.924594 c 0.184911,0 0.331571,-0.14666 0.331571,-0.331577 v -4.744098 c 0,-0.184918 0.146661,-0.331577 0.331582,-0.331577 h 2.894913 c 0.184921,0 0.331582,0.146659 0.331582,0.331577 v 4.744098 c 0,0.184917 0.146661,0.331577 0.331571,0.331577 h 0.446363 c 0.18491,0 0.331571,-0.14666 0.331571,-0.331577 v -5.636804 c 0,-0.184918 -0.146661,-0.331577 -0.331571,-0.331577 H 4.9079695 c -0.1849211,0 -0.3315819,0.146659 -0.3315819,0.331577 z m 1.6578879,-4.852498 h 5.6495565 c 0.15303,0 0.280561,0.12753 0.280561,0.280564 v 3.513438 c 0,0.153036 -0.127531,0.280566 -0.280561,0.280566 H 6.2342755 c -0.1530412,0 -0.2805719,-0.12753 -0.2805719,-0.280566 v -3.513438 c 0,-0.159411 0.1275307,-0.280564 0.2805719,-0.280564 z M 19.790657,3.3879075 H 4.8569594 c -0.1530412,0 -0.2805718,-0.1275296 -0.2805718,-0.2805642 V 1.3665653 C 4.5763876,1.2135296 4.7039182,1.086 4.8569594,1.086 H 19.790657 c 0.153041,0 0.280572,0.1275296 0.280572,0.2805653 v 1.740778 c 0,0.1530346 -0.127531,0.2805642 -0.280572,0.2805642 z",
keywords: ["hacs", "home assistant community store"],
},
};
window.customIcons = window.customIcons || {};
window.customIconsets = window.customIconsets || {};
window.customIcons["hacs"] = {
getIcon: async (iconName) => (
{ path: hacsIcons[iconName]?.path }
),
getIconList: async () =>
Object.entries(hacsIcons).map(([icon, content]) => ({
name: icon,
keywords: content.keywords,
})
)
};
================================================
FILE: custom_components/hacs/manifest.json
================================================
{
"domain": "hacs",
"name": "HACS",
"after_dependencies": [
"python_script"
],
"codeowners": [
"@ludeeus"
],
"config_flow": true,
"dependencies": [
"http",
"websocket_api",
"frontend",
"persistent_notification",
"lovelace",
"repairs"
],
"documentation": "https://hacs.xyz/docs/use/",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/hacs/integration/issues",
"requirements": [
"aiogithubapi>=22.10.1"
],
"version": "0.0.0"
}
================================================
FILE: custom_components/hacs/repairs.py
================================================
"""Repairs platform for HACS."""
from __future__ import annotations
from typing import Any
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.core import HomeAssistant
import voluptuous as vol
from custom_components.hacs.base import HacsBase
from .const import DOMAIN
class RestartRequiredFixFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, issue_id: str) -> None:
self.issue_id = issue_id
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm_restart()
async def async_step_confirm_restart(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
await self.hass.services.async_call("homeassistant", "restart")
return self.async_create_entry(title="", data={})
hacs: HacsBase = self.hass.data[DOMAIN]
integration = hacs.repositories.get_by_id(self.issue_id.split("_")[2])
return self.async_show_form(
step_id="confirm_restart",
data_schema=vol.Schema({}),
description_placeholders={"name": integration.display_name},
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None = None,
*args: Any,
**kwargs: Any,
) -> RepairsFlow | None:
"""Create flow."""
if issue_id.startswith("restart_required"):
return RestartRequiredFixFlow(issue_id)
return None
================================================
FILE: custom_components/hacs/repositories/__init__.py
================================================
"""Initialize repositories."""
from __future__ import annotations
from ..enums import HacsCategory
from .appdaemon import HacsAppdaemonRepository
from .base import HacsRepository
from .integration import HacsIntegrationRepository
from .plugin import HacsPluginRepository
from .python_script import HacsPythonScriptRepository
from .template import HacsTemplateRepository
from .theme import HacsThemeRepository
REPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = {
HacsCategory.THEME: HacsThemeRepository,
HacsCategory.INTEGRATION: HacsIntegrationRepository,
HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository,
HacsCategory.APPDAEMON: HacsAppdaemonRepository,
HacsCategory.PLUGIN: HacsPluginRepository,
HacsCategory.TEMPLATE: HacsTemplateRepository,
}
================================================
FILE: custom_components/hacs/repositories/appdaemon.py
================================================
"""Class for appdaemon apps in HACS."""
from __future__ import annotations
from typing import TYPE_CHECKING
from aiogithubapi import AIOGitHubAPIException
from ..enums import HacsCategory, HacsDispatchEvent
from ..exceptions import HacsException
from ..utils.decorator import concurrent
from .base import HacsRepository
if TYPE_CHECKING:
from ..base import HacsBase
class HacsAppdaemonRepository(HacsRepository):
"""Appdaemon apps in HACS."""
def __init__(self, hacs: HacsBase, full_name: str):
"""Initialize."""
super().__init__(hacs=hacs)
self.data.full_name = full_name
self.data.full_name_lower = full_name.lower()
self.data.category = HacsCategory.APPDAEMON
self.content.path.local = self.localpath
self.content.path.remote = "apps"
@property
def localpath(self):
"""Return localpath."""
return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.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 AIOGitHubAPIException:
raise HacsException(
f"{self.string} Repository structure for {self.ref.replace('tags/', '')} is not compliant"
) from None
if not isinstance(addir, list):
self.validate.errors.append(f"{self.string} Repository structure not compliant")
self.content.path.remote = addir[0].path
self.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
# Handle potential errors
if self.validate.errors:
for error in self.validate.errors:
if not self.hacs.status.startup:
self.logger.error("%s %s", self.string, error)
return self.validate.success
@concurrent(concurrenttasks=10, backoff_time=5)
async def update_repository(self, ignore_issues=False, force=False):
"""Update."""
if not await self.common_update(ignore_issues, force) and not force:
return
# Get appdaemon objects.
if self.repository_manifest:
if self.repository_manifest.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.content.objects = await self.repository_object.get_contents(
self.content.path.remote, self.ref
)
# Set local path
self.content.path.local = self.localpath
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"id": 1337,
"action": "update",
"repository": self.data.full_name,
"repository_id": self.data.id,
},
)
================================================
FILE: custom_components/hacs/repositories/base.py
================================================
"""Repository."""
from __future__ import annotations
from asyncio import sleep
from datetime import UTC, datetime
import os
import pathlib
import shutil
import tempfile
from typing import TYPE_CHECKING, Any
import zipfile
from aiogithubapi import (
AIOGitHubAPIException,
AIOGitHubAPINotModifiedException,
GitHubReleaseModel,
)
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
import attr
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from ..const import DOMAIN
from ..enums import HacsDispatchEvent, RepositoryFile
from ..exceptions import (
HacsException,
HacsNotModifiedException,
HacsRepositoryArchivedException,
HacsRepositoryExistException,
)
from ..types import DownloadableContent
from ..utils.backup import Backup
from ..utils.decode import decode_content
from ..utils.decorator import concurrent, return_none_on_exception
from ..utils.file_system import async_exists, async_remove, async_remove_directory
from ..utils.filters import filter_content_return_one_of_type
from ..utils.github_graphql_query import GET_REPOSITORY_RELEASES
from ..utils.json import json_loads
from ..utils.logger import LOGGER
from ..utils.path import is_safe
from ..utils.queue_manager import QueueManager
from ..utils.store import async_remove_store
from ..utils.url import github_archive, github_release_asset
from ..utils.validate import Validate
from ..utils.version import (
version_left_higher_or_equal_then_right,
version_left_higher_then_right,
)
from ..utils.workarounds import DOMAIN_OVERRIDES
if TYPE_CHECKING:
from ..base import HacsBase
TOPIC_FILTER = (
"add-on",
"addon",
"app",
"appdaemon-apps",
"appdaemon",
"custom-card",
"custom-cards",
"custom-component",
"custom-components",
"customcomponents",
"hacktoberfest",
"hacs-default",
"hacs-integration",
"hacs-repository",
"hacs",
"hass",
"hassio",
"home-assistant-custom",
"home-assistant-frontend",
"home-assistant-hacs",
"home-assistant-sensor",
"home-assistant",
"home-automation",
"homeassistant-components",
"homeassistant-integration",
"homeassistant-sensor",
"homeassistant",
"homeautomation",
"integration",
"lovelace-ui",
"lovelace",
"media-player",
"mediaplayer",
"plugin",
"python_script",
"python-script",
"python",
"sensor",
"smart-home",
"smarthome",
"template",
"templates",
"theme",
"themes",
)
REPOSITORY_KEYS_TO_EXPORT = (
# Keys can not be removed from this list until v3
# If keys are added, the action need to be re-run with force
("description", ""),
("downloads", 0),
("domain", None),
("etag_releases", None),
("etag_repository", None),
("full_name", ""),
("last_commit", None),
("last_updated", 0),
("last_version", None),
("manifest_name", None),
("open_issues", 0),
("prerelease", None),
("stargazers_count", 0),
("topics", []),
)
HACS_MANIFEST_KEYS_TO_EXPORT = (
# Keys can not be removed from this list until v3
# If keys are added, the action need to be re-run with force
("country", []),
("name", None),
)
class FileInformation:
"""FileInformation."""
def __init__(self, url, path, name):
self.download_url = url
self.path = path
self.name = name
@attr.s(auto_attribs=True)
class RepositoryData:
"""RepositoryData class."""
archived: bool = False
authors: list[str] = []
category: str = ""
config_flow: bool = False
default_branch: str = None
description: str = ""
domain: str = None
downloads: int = 0
etag_repository: str = None
etag_releases: str = None
file_name: str = ""
first_install: bool = False
full_name: str = ""
hide: bool = False
has_issues: bool = True
id: int = 0
installed_commit: str = None
installed_version: str = None
installed: bool = False
last_commit: str = None
last_fetched: datetime = None
last_updated: str = 0
last_version: str = None
manifest_name: str = None
new: bool = True
open_issues: int = 0
prerelease: str = None
published_tags: list[str] = []
releases: bool = False
selected_tag: str = None
show_beta: bool = False
stargazers_count: int = 0
topics: list[str] = []
@property
def name(self):
"""Return the name."""
if self.category == "integration":
return self.domain
return self.full_name.split("/")[-1]
def to_json(self):
"""Export to json."""
return attr.asdict(self, filter=lambda attr, value: attr.name != "last_fetched")
@staticmethod
def create_from_dict(source: dict, action: bool = False) -> RepositoryData:
"""Set attributes from dicts."""
data = RepositoryData()
data.update_data(source, action)
return data
def update_data(self, data: dict, action: bool = False) -> None:
"""Update data of the repository."""
for key, value in data.items():
if key not in self.__dict__:
continue
if key == "last_fetched" and isinstance(value, float):
setattr(self, key, datetime.fromtimestamp(value, UTC))
elif key == "id":
setattr(self, key, str(value))
elif key == "country":
if isinstance(value, str):
setattr(self, key, [value])
else:
setattr(self, key, value)
elif key == "topics" and not action:
setattr(self, key, [topic for topic in value if topic not in TOPIC_FILTER])
else:
setattr(self, key, value)
@attr.s(auto_attribs=True)
class HacsManifest:
"""HacsManifest class."""
content_in_root: bool = False
country: list[str] = []
filename: str = None
hacs: str = None # Minimum HACS version
hide_default_branch: bool = False
homeassistant: str = None # Minimum Home Assistant version
manifest: dict = {}
name: str = None
persistent_directory: str = None
render_readme: bool = False
zip_release: bool = False
def to_dict(self):
"""Export to json."""
return attr.asdict(self)
@staticmethod
def from_dict(manifest: dict):
"""Set attributes from dicts."""
if manifest is None:
raise HacsException("Missing manifest data")
manifest_data = HacsManifest()
manifest_data.manifest = {
k: v
for k, v in manifest.items()
if k in manifest_data.__dict__ and v != manifest_data.__getattribute__(k)
}
for key, value in manifest_data.manifest.items():
if key == "country" and isinstance(value, str):
setattr(manifest_data, key, [value])
elif key in manifest_data.__dict__:
setattr(manifest_data, key, value)
return manifest_data
def update_data(self, data: dict) -> None:
"""Update the manifest data."""
for key, value in data.items():
if key not in self.__dict__:
continue
if key == "country":
if isinstance(value, str):
setattr(self, key, [value])
else:
setattr(self, key, value)
else:
setattr(self, key, value)
class RepositoryReleases:
"""RepositoyReleases."""
last_release = None
last_release_object = None
published_tags = []
objects: list[GitHubReleaseModel] = []
releases = False
downloads = None
class RepositoryPath:
"""RepositoryPath."""
local: str | None = None
remote: str | None = None
class RepositoryContent:
"""RepositoryContent."""
path: RepositoryPath | None = None
files = []
objects = []
single = False
class HacsRepository:
"""HacsRepository."""
def __init__(self, hacs: HacsBase) -> None:
"""Set up HacsRepository."""
self.hacs = hacs
self.additional_info = ""
self.data = RepositoryData()
self.content = RepositoryContent()
self.content.path = RepositoryPath()
self.repository_object: AIOGitHubAPIRepository | None = None
self.updated_info = False
self.state = None
self.force_branch = False
self.integration_manifest = {}
self.repository_manifest = HacsManifest.from_dict({})
self.validate = Validate()
self.releases = RepositoryReleases()
self.pending_restart = False
self.tree = []
self.treefiles = []
self.ref = None
self.logger = LOGGER
def __str__(self) -> str:
"""Return a string representation of the repository."""
return self.string
@property
def string(self) -> str:
"""Return a string representation of the repository."""
return f"<{self.data.category.title()} {self.data.full_name}>"
@property
def display_name(self) -> str:
"""Return display name."""
if self.repository_manifest.name is not None:
return self.repository_manifest.name
if self.data.category == "integration":
if self.data.manifest_name is not None:
return self.data.manifest_name
if "name" in self.integration_manifest:
return self.integration_manifest["name"]
return self.data.full_name.split("/")[-1].replace("-", " ").replace("_", " ").title()
@property
def ignored_by_country_configuration(self) -> bool:
"""Return True if hidden by country."""
if self.data.installed:
return False
configuration = self.hacs.configuration.country.lower()
if configuration == "all":
return False
manifest = [entry.lower() for entry in self.repository_manifest.country or []]
if not manifest:
return False
return configuration not in manifest
@property
def display_status(self) -> str:
"""Return display_status."""
if self.data.new:
status = "new"
elif self.pending_restart:
status = "pending-restart"
elif self.pending_update:
status = "pending-upgrade"
elif self.data.installed:
status = "installed"
else:
status = "default"
return status
@property
def display_installed_version(self) -> str:
"""Return display_authors"""
if self.data.installed_version is not None:
installed = self.data.installed_version
else:
if self.data.installed_commit is not None:
installed = self.data.installed_commit
else:
installed = ""
return str(installed)
@property
def display_available_version(self) -> str:
"""Return display_authors"""
if self.data.show_beta and self.data.prerelease is not None:
available = self.data.prerelease
elif self.data.last_version is not None:
available = self.data.last_version
else:
if self.data.last_commit is not None:
available = self.data.last_commit
else:
available = ""
return str(available)
@property
def display_version_or_commit(self) -> str:
"""Does the repositoriy use releases or commits?"""
if self.data.releases:
version_or_commit = "version"
else:
version_or_commit = "commit"
return version_or_commit
@property
def pending_update(self) -> bool:
"""Return True if pending update."""
if self.data.installed:
if self.data.selected_tag is not None:
if self.data.selected_tag == self.data.default_branch:
if self.data.installed_commit != self.data.last_commit:
return True
return False
if self.display_version_or_commit == "version":
if (
result := version_left_higher_then_right(
self.display_available_version,
self.display_installed_version,
)
) is not None:
return result
if self.display_installed_version != self.display_available_version:
return True
return False
@property
def can_download(self) -> bool:
"""Return True if we can download."""
if self.repository_manifest.homeassistant is not None:
if self.data.releases:
if not version_left_higher_or_equal_then_right(
self.hacs.core.ha_version.string,
self.repository_manifest.homeassistant,
):
return False
return True
@property
def localpath(self) -> str | None:
"""Return localpath."""
return None
@property
def should_try_releases(self) -> bool:
"""Return a boolean indicating whether to download releases or not."""
if self.repository_manifest.zip_release:
if self.repository_manifest.filename.endswith(".zip"):
if self.ref != self.data.default_branch:
return True
if self.ref == self.data.default_branch:
return False
if self.data.category not in ["plugin", "theme"]:
return False
if not self.data.releases:
return False
return True
async def validate_repository(self) -> None:
"""Validate."""
@concurrent(concurrenttasks=10, backoff_time=5)
async def update_repository(self, ignore_issues=False, force=False) -> None:
"""Update the repository"""
async def common_validate(self, ignore_issues: bool = False) -> None:
"""Common validation steps of the repository."""
self.validate.errors.clear()
# Make sure the repository exist.
self.logger.debug("%s Checking repository.", self.string)
await self.common_update_data(ignore_issues=ignore_issues)
# Get the content of hacs.json
if RepositoryFile.HACS_JSON in [x.filename for x in self.tree]:
if manifest := await self.async_get_hacs_json():
self.repository_manifest = HacsManifest.from_dict(manifest)
self.data.update_data(
self.repository_manifest.to_dict(),
action=self.hacs.system.action,
)
async def common_registration(self) -> None:
"""Common registration steps of the repository."""
# Attach repository
if self.repository_object is None:
try:
self.repository_object, etag = await self.async_get_legacy_repository_object(
etag=None if self.data.installed else self.data.etag_repository,
)
self.data.update_data(
self.repository_object.attributes,
action=self.hacs.system.action,
)
self.data.etag_repository = etag
except HacsNotModifiedException:
self.logger.debug("%s Did not update, content was not modified", self.string)
return
if self.repository_object:
self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0)
self.data.last_fetched = datetime.now(UTC)
@concurrent(concurrenttasks=10, backoff_time=5)
async def common_update(self, ignore_issues=False, force=False, skip_releases=False) -> bool:
"""Common information update steps of the repository."""
self.logger.debug("%s Getting repository information", self.string)
# Attach repository
current_etag = self.data.etag_repository
try:
await self.common_update_data(
ignore_issues=ignore_issues,
force=force,
skip_releases=skip_releases,
)
except HacsRepositoryExistException:
self.data.full_name = self.hacs.common.renamed_repositories[self.data.full_name]
await self.common_update_data(ignore_issues=ignore_issues, force=force)
except HacsException:
if not ignore_issues and not force:
return False
if not self.data.installed and (current_etag == self.data.etag_repository) and not force:
self.logger.debug("%s Did not update, content was not modified", self.string)
return False
# Update last updated
if self.repository_object:
self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0)
# Update last available commit
await self.repository_object.set_last_commit()
self.data.last_commit = self.repository_object.last_commit
# Get the content of hacs.json
if RepositoryFile.HACS_JSON in [x.filename for x in self.tree]:
if manifest := await self.async_get_hacs_json():
self.repository_manifest = HacsManifest.from_dict(manifest)
self.data.update_data(
self.repository_manifest.to_dict(),
action=self.hacs.system.action,
)
# Update "info.md"
self.additional_info = await self.async_get_info_file_contents()
# Set last fetch attribute
self.data.last_fetched = datetime.now(UTC)
return True
async def download_zip_files(self, validate: Validate) -> None:
"""Download ZIP archive from repository release."""
try:
await self.async_download_zip_file(
DownloadableContent(
name=self.repository_manifest.filename,
url=github_release_asset(
repository=self.data.full_name,
version=self.ref,
filename=self.repository_manifest.filename,
),
),
validate,
)
# lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
validate.errors.append(
f"Download of {self.repository_manifest.filename} was not completed"
)
async def async_download_zip_file(
self,
content: DownloadableContent,
validate: Validate,
) -> None:
"""Download ZIP archive from repository release."""
try:
filecontent = await self.hacs.async_download_file(content["url"])
if filecontent is None:
validate.errors.append(f"Failed to download {content['url']}")
return
temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp)
temp_file = f"{temp_dir}/{self.repository_manifest.filename}"
result = await self.hacs.async_save_file(temp_file, filecontent)
def _extract_zip_file():
with zipfile.ZipFile(temp_file, "r") as zip_file:
zip_file.extractall(self.content.path.local)
await self.hacs.hass.async_add_executor_job(_extract_zip_file)
def cleanup_temp_dir():
"""Cleanup temp_dir."""
if os.path.exists(temp_dir):
self.logger.debug("%s Cleaning up %s", self.string, temp_dir)
shutil.rmtree(temp_dir)
if result:
self.logger.info("%s Download of %s completed", self.string, content["name"])
await self.hacs.hass.async_add_executor_job(cleanup_temp_dir)
return
validate.errors.append(f"[{content['name']}] was not downloaded")
# lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
validate.errors.append("Download was not completed")
async def download_content(self, version: string | None = None) -> None:
"""Download the content of a directory."""
contents: list[FileInformation] | None = None
if (
not self.repository_manifest.zip_release
and not self.data.file_name
and self.content.path.remote is not None
):
self.logger.info("%s Downloading repository archive", self.string)
try:
await self.download_repository_zip()
return
except HacsException as exception:
self.logger.exception(exception)
if self.repository_manifest.filename:
self.logger.debug("%s %s", self.string, self.repository_manifest.filename)
if self.content.path.remote == "release" and version is not None:
contents = await self.release_contents(version)
if not contents:
contents = self.gather_files_to_download()
if not contents:
raise HacsException("No content to download")
download_queue = QueueManager(hass=self.hacs.hass)
for content in contents:
if self.repository_manifest.content_in_root and self.repository_manifest.filename:
if content.name != self.repository_manifest.filename:
continue
download_queue.add(self.dowload_repository_content(content))
await download_queue.execute()
async def download_repository_zip(self):
"""Download the zip archive of the repository."""
ref = f"{self.ref}".replace("tags/", "")
if not ref:
raise HacsException("Missing required elements.")
filecontent = await self.hacs.async_download_file(
github_archive(repository=self.data.full_name, version=ref, variant="tags"),
keep_url=True,
nolog=True,
)
if filecontent is None:
filecontent = await self.hacs.async_download_file(
github_archive(repository=self.data.full_name, version=ref, variant="heads"),
keep_url=True,
)
if filecontent is None:
raise HacsException(f"[{self}] Failed to download zipball")
temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp)
temp_file = f"{temp_dir}/{self.repository_manifest.filename}"
result = await self.hacs.async_save_file(temp_file, filecontent)
if not result:
raise HacsException("Could not save ZIP file")
def _extract_zip_file():
with zipfile.ZipFile(temp_file, "r") as zip_file:
extractable = []
for path in zip_file.filelist:
filename = "/".join(path.filename.split("/")[1:])
if (
filename.startswith(self.content.path.remote)
and filename != self.content.path.remote
):
path.filename = filename.replace(self.content.path.remote, "")
if path.filename == "/":
# Blank files is not valid, and will start to throw in Python 3.12
continue
extractable.append(path)
if len(extractable) == 0:
raise HacsException("No content to extract")
zip_file.extractall(self.content.path.local, extractable)
await self.hacs.hass.async_add_executor_job(_extract_zip_file)
def cleanup_temp_dir():
"""Cleanup temp_dir."""
if os.path.exists(temp_dir):
self.logger.debug("%s Cleaning up %s", self.string, temp_dir)
shutil.rmtree(temp_dir)
await self.hacs.hass.async_add_executor_job(cleanup_temp_dir)
self.logger.info("%s Content was extracted to %s", self.string, self.content.path.local)
async def async_get_hacs_json(self, ref: str = None) -> dict[str, Any] | None:
"""Get the content of the hacs.json file."""
try:
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.contents.get,
raise_exception=False,
repository=self.data.full_name,
path=RepositoryFile.HACS_JSON,
**{"params": {"ref": ref or self.version_to_download()}},
)
if response:
return json_loads(decode_content(response.data.content))
# lgtm [py/catch-base-exception] pylint: disable=broad-except
except BaseException:
pass
async def async_get_info_file_contents(self, *, version: str | None = None, **kwargs) -> str:
"""Get the content of the info.md file."""
def _info_file_variants() -> tuple[str, ...]:
name: str = "readme"
return (
f"{name.upper()}.md",
f"{name}.md",
f"{name}.MD",
f"{name.upper()}.MD",
name.upper(),
name,
)
info_files = [filename for filename in _info_file_variants() if filename in self.treefiles]
if not info_files:
return ""
return await self.get_documentation(filename=info_files[0], version=version) or ""
def remove(self) -> None:
"""Run remove tasks."""
if self.hacs.repositories.is_registered(repository_id=str(self.data.id)):
self.logger.info("%s Starting removal", self.string)
self.hacs.repositories.unregister(self)
async def uninstall(self) -> None:
"""Run uninstall tasks."""
self.logger.info("%s Removing", self.string)
if not await self.remove_local_directory():
raise HacsException("Could not uninstall")
self.data.installed = False
await self._async_post_uninstall()
await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs")
self.data.installed_version = None
self.data.installed_commit = None
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"id": 1337,
"action": "uninstall",
"repository": self.data.full_name,
"repository_id": self.data.id,
},
)
await self.async_remove_entity_device()
ir.async_delete_issue(self.hacs.hass, DOMAIN, f"removed_{self.data.id}")
async def remove_local_directory(self) -> None:
"""Check the local directory."""
try:
if self.data.category == "python_script":
local_path = f"{self.content.path.local}/{self.data.file_name}"
elif self.data.category == "template":
local_path = f"{self.content.path.local}/{self.data.file_name}"
elif self.data.category == "theme":
path = (
f"{self.hacs.core.config_path}/"
f"{self.hacs.configuration.theme_path}/"
f"{self.data.name}.yaml"
)
await async_remove(self.hacs.hass, path, missing_ok=True)
local_path = self.content.path.local
elif self.data.category == "integration":
if not self.data.domain:
if domain := DOMAIN_OVERRIDES.get(self.data.full_name):
self.data.domain = domain
self.content.path.local = self.localpath
else:
self.logger.error("%s Missing domain", self.string)
return False
local_path = self.content.path.local
else:
local_path = self.content.path.local
if await async_exists(self.hacs.hass, local_path):
if not is_safe(self.hacs, local_path):
self.logger.error("%s Path %s is blocked from removal", self.string, local_path)
return False
self.logger.debug("%s Removing %s", self.string, local_path)
if self.data.category in ["python_script", "template"]:
await async_remove(self.hacs.hass, local_path)
else:
await async_remove_directory(self.hacs.hass, local_path)
while await async_exists(self.hacs.hass, local_path):
await sleep(1)
else:
self.logger.debug(
"%s Presumed local content path %s does not exist", self.string, local_path
)
except (
# lgtm [py/catch-base-exception] pylint: disable=broad-except
BaseException
) as exception:
self.logger.debug("%s Removing %s failed with %s", self.string, local_path, exception)
return False
return True
async def async_pre_registration(self) -> None:
"""Run pre registration steps."""
@concurrent(concurrenttasks=10)
async def async_registration(self, ref=None) -> None:
"""Run registration steps."""
await self.async_pre_registration()
if ref is not None:
self.data.selected_tag = ref
self.ref = ref
self.force_branch = True
if not await self.validate_repository():
return False
# Run common registration steps.
await self.common_registration()
# Set correct local path
self.content.path.local = self.localpath
# Run local post registration steps.
await self.async_post_registration()
async def async_post_registration(self) -> None:
"""Run post registration steps."""
if not self.hacs.system.action:
return
await self.hacs.validation.async_run_repository_checks(self)
async def async_pre_install(self) -> None:
"""Run pre install steps."""
async def _async_pre_install(self) -> None:
"""Run pre install steps."""
self.logger.info("%s Running pre installation steps", self.string)
await self.async_pre_install()
self.logger.info("%s Pre installation steps completed", self.string)
async def async_install(self, *, version: str | None = None, **_) -> None:
"""Run install steps."""
await self._async_pre_install()
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 30},
)
self.logger.info("%s Running installation steps", self.string)
await self.async_install_repository(version=version)
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 90},
)
self.logger.info("%s Installation steps completed", self.string)
await self._async_post_install()
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": False},
)
async def async_post_installation(self) -> None:
"""Run post install steps."""
async def async_post_uninstall(self):
"""Run post uninstall steps."""
async def _async_post_uninstall(self):
"""Run post uninstall steps."""
await self.async_post_uninstall()
async def _async_post_install(self) -> None:
"""Run post install steps."""
self.logger.info("%s Running post installation steps", self.string)
await self.async_post_installation()
self.data.new = False
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
{
"id": 1337,
"action": "install",
"repository": self.data.full_name,
"repository_id": self.data.id,
},
)
self.logger.info("%s Post installation steps completed", self.string)
async def async_install_repository(self, *, version: str | None = None, **_) -> None:
"""Common installation steps of the repository."""
persistent_directory = None
force_update = version is None or (
self.data.last_version is not None and version != self.data.last_version
)
await self.update_repository(force=force_update)
if self.content.path.local is None:
raise HacsException("repository.content.path.local is None")
self.validate.errors.clear()
version_to_install = version or self.version_to_download()
if version_to_install == self.data.default_branch:
self.ref = version_to_install
else:
self.ref = f"tags/{version_to_install}"
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 40},
)
if self.repository_manifest.persistent_directory:
if await async_exists(
self.hacs.hass,
f"{self.content.path.local}/{self.repository_manifest.persistent_directory}",
):
persistent_directory = Backup(
hacs=self.hacs,
local_path=f"{self.content.path.local}/{self.repository_manifest.persistent_directory}",
backup_path=tempfile.gettempdir() + "/hacs_persistent_directory/",
)
await self.hacs.hass.async_add_executor_job(persistent_directory.create)
if self.data.installed and not self.content.single:
backup = Backup(hacs=self.hacs, local_path=self.content.path.local)
await self.hacs.hass.async_add_executor_job(backup.create)
self.hacs.log.debug("%s Local path is set to %s", self.string, self.content.path.local)
self.hacs.log.debug("%s Remote path is set to %s", self.string, self.content.path.remote)
self.hacs.log.debug("%s Version to install: %s", self.string, version_to_install)
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 50},
)
if self.repository_manifest.zip_release and self.repository_manifest.filename:
await self.download_zip_files(self.validate)
else:
await self.download_content(version_to_install)
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 70},
)
if self.validate.errors:
for error in self.validate.errors:
self.logger.error("%s %s", self.string, error)
if self.data.installed and not self.content.single:
await self.hacs.hass.async_add_executor_job(backup.restore)
await self.hacs.hass.async_add_executor_job(backup.cleanup)
raise HacsException("Could not download, see log for details")
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS,
{"repository": self.data.full_name, "progress": 80},
)
if self.data.installed and not self.content.single:
await self.hacs.hass.async_add_executor_job(backup.cleanup)
if persistent_directory is not None:
await self.hacs.hass.async_add_executor_job(persistent_directory.restore)
await self.hacs.hass.async_add_executor_job(persistent_directory.cleanup)
if self.validate.success:
self.data.installed = True
self.data.installed_commit = self.data.last_commit
if version_to_install == self.data.default_branch:
self.data.installed_version = None
else:
self.data.installed_version = version_to_install
async def async_get_legacy_repository_object(
self,
etag: str | None = None,
) -> tuple[AIOGitHubAPIRepository, Any | None]:
"""Return a repository object."""
try:
repository = await self.hacs.github.get_repo(self.data.full_name, etag)
return repository, self.hacs.github.client.last_response.etag
except AIOGitHubAPINotModifiedException as exception:
raise HacsNotModifiedException(exception) from exception
except (ValueError, AIOGitHubAPIException, Exception) as exception:
raise HacsException(exception) from exception
def update_filenames(self) -> None:
"""Get the filename to target."""
async def get_tree(self, ref: str):
"""Return the repository tree."""
if self.repository_object is None:
raise HacsException("No repository_object")
try:
tree = await self.repository_object.get_tree(ref)
return tree
except (ValueError, AIOGitHubAPIException) as exception:
raise HacsException(exception) from exception
async def get_releases(self, prerelease=False, returnlimit=5) -> list[GitHubReleaseModel]:
"""Return the repository releases."""
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.releases.list,
repository=self.data.full_name,
)
releases = []
for release in response.data or []:
if len(releases) == returnlimit:
break
if release.draft or (release.prerelease and not prerelease):
continue
releases.append(release)
return releases
async def common_update_data(
self,
ignore_issues: bool = False,
force: bool = False,
retry=False,
skip_releases=False,
) -> None:
"""Common update data."""
releases = []
try:
repository_object, etag = await self.async_get_legacy_repository_object(
etag=None if force or self.data.installed else self.data.etag_repository,
)
self.repository_object = repository_object
if self.data.full_name.lower() != repository_object.full_name.lower():
self.hacs.common.renamed_repositories[self.data.full_name] = (
repository_object.full_name
)
if not self.hacs.system.generator:
raise HacsRepositoryExistException
self.logger.error(
"%s Repository has been renamed - %s", self.string, repository_object.full_name
)
self.data.update_data(
repository_object.attributes,
action=self.hacs.system.action,
)
self.data.etag_repository = etag
except HacsNotModifiedException:
return
except HacsRepositoryExistException:
raise HacsRepositoryExistException from None
except (AIOGitHubAPIException, HacsException) as exception:
if not self.hacs.status.startup or self.hacs.system.generator:
self.logger.error("%s %s", self.string, exception)
if not ignore_issues:
self.validate.errors.append("Repository does not exist.")
raise HacsException(exception) from exception
# Make sure the repository is not archived.
if self.data.archived and not ignore_issues:
self.validate.errors.append("Repository is archived.")
if self.data.full_name not in self.hacs.common.archived_repositories:
self.hacs.common.archived_repositories.add(self.data.full_name)
raise HacsRepositoryArchivedException(f"{self} Repository is archived.")
# Make sure the repository is not in the blacklist.
if self.hacs.repositories.is_removed(self.data.full_name):
removed = self.hacs.repositories.removed_repository(self.data.full_name)
if removed.removal_type != "remove" and not ignore_issues:
self.validate.errors.append("Repository has been requested to be removed.")
raise HacsException(f"{self} Repository has been requested to be removed.")
# Get releases.
if not skip_releases:
try:
releases = await self.get_releases(prerelease=True, returnlimit=30)
if releases:
self.data.prerelease = None
for release in releases:
if release.draft:
continue
elif release.prerelease:
if self.data.prerelease is None:
self.data.prerelease = release.tag_name
else:
self.data.last_version = release.tag_name
break
self.data.releases = True
filtered_releases = [
release
for release in releases
if not release.draft and (self.data.show_beta or not release.prerelease)
]
self.releases.objects = filtered_releases
self.data.published_tags = [x.tag_name for x in filtered_releases]
except HacsException:
self.data.releases = False
if not self.force_branch:
self.ref = self.version_to_download()
if self.data.releases:
for release in self.releases.objects or []:
if release.tag_name == self.ref:
if assets := release.assets:
downloads = next(iter(assets)).download_count
self.data.downloads = downloads
elif self.hacs.system.generator and self.repository_object:
await self.repository_object.set_last_commit()
self.data.last_commit = self.repository_object.last_commit
self.hacs.log.debug(
"%s Running checks against %s", self.string, self.ref.replace("tags/", "")
)
try:
self.tree = await self.get_tree(self.ref)
if not self.tree:
raise HacsException("No files in tree")
self.treefiles = []
for treefile in self.tree:
self.treefiles.append(treefile.full_path)
except (AIOGitHubAPIException, HacsException) as exception:
if (
not retry
and self.ref is not None
and str(exception).startswith("GitHub returned 404")
):
# Handle tags/branches being deleted.
self.data.selected_tag = None
self.ref = self.version_to_download()
self.logger.warning(
"%s Selected version/branch %s has been removed, falling back to default",
self.string,
self.ref,
)
return await self.common_update_data(ignore_issues, force, True)
if not self.hacs.status.startup and not ignore_issues:
self.logger.error("%s %s", self.string, exception)
if not ignore_issues:
raise HacsException(exception) from None
def gather_files_to_download(self) -> list[FileInformation]:
"""Return a list of file objects to be downloaded."""
files = []
tree = self.tree
ref = f"{self.ref}".replace("tags/", "")
releaseobjects = self.releases.objects
category = self.data.category
remotelocation = self.content.path.remote
if self.should_try_releases:
for release in releaseobjects or []:
if ref == release.tag_name:
for asset in release.assets or []:
files.append(
FileInformation(asset.browser_download_url, asset.name, asset.name)
)
if files:
return files
if self.content.single:
for treefile in tree:
if treefile.filename == self.data.file_name:
files.append(
FileInformation(
treefile.download_url, treefile.full_path, treefile.filename
)
)
return files
if category == "plugin":
for treefile in tree:
if treefile.path in ["", "dist"]:
if remotelocation == "dist" and not treefile.filename.startswith("dist"):
continue
if not remotelocation:
if not treefile.filename.endswith(".js"):
continue
if treefile.path != "":
continue
if not treefile.is_directory:
files.append(
FileInformation(
treefile.download_url, treefile.full_path, treefile.filename
)
)
if files:
return files
if self.repository_manifest.content_in_root:
if not self.repository_manifest.filename:
if category == "theme":
tree = filter_content_return_one_of_type(self.tree, "", "yaml", "full_path")
for path in tree:
if path.is_directory:
continue
if path.full_path.startswith(self.content.path.remote):
files.append(FileInformation(path.download_url, path.full_path, path.filename))
return files
async def release_contents(self, version: str | None = None) -> list[FileInformation] | None:
"""Gather the contents of a release."""
release = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.generic,
endpoint=f"/repos/{self.data.full_name}/releases/tags/{version}",
raise_exception=False,
)
if release is None:
return None
return [
FileInformation(
url=asset.get("browser_download_url"),
path=asset.get("name"),
name=asset.get("name"),
)
for asset in release.data.get("assets", [])
]
@concurrent(concurrenttasks=10)
async def dowload_repository_content(self, content: FileInformation) -> None:
"""Download content."""
try:
self.logger.debug("%s Downloading %s", self.string, content.name)
filecontent = await self.hacs.async_download_file(content.download_url)
if filecontent is None:
self.validate.errors.append(f"[{content.name}] was not downloaded.")
return
# Save the content of the file.
if self.content.single or content.path is None:
local_directory = self.content.path.local
else:
_content_path = content.path
if not self.repository_manifest.content_in_root:
_content_pa
gitextract_ec9nfn8q/
├── .codeclimate.yml
├── .codecov.yml
├── .coveragerc
├── .devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── a_integration.yml
│ │ ├── b_frontend.yml
│ │ ├── c_bot.yml
│ │ ├── config.yml
│ │ ├── d_documentation.yml
│ │ ├── e_action.yml
│ │ ├── f_addon.yml
│ │ └── removal.yml
│ ├── dependabot.yml
│ ├── pre-commit-config.yaml
│ ├── release.yml
│ └── workflows/
│ ├── action-container.yml
│ ├── generate-hacs-data.yml
│ ├── lint.yaml
│ ├── lock.yml
│ ├── publish.yml
│ ├── pull_requests_labels.yml
│ ├── pytest.yml
│ ├── stale.yml
│ └── validate.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── README.md
├── action/
│ ├── Dockerfile
│ └── action.py
├── constraints.txt
├── custom_components/
│ └── hacs/
│ ├── __init__.py
│ ├── base.py
│ ├── config_flow.py
│ ├── const.py
│ ├── coordinator.py
│ ├── data_client.py
│ ├── diagnostics.py
│ ├── entity.py
│ ├── enums.py
│ ├── exceptions.py
│ ├── frontend.py
│ ├── icons.json
│ ├── iconset.js
│ ├── manifest.json
│ ├── repairs.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── appdaemon.py
│ │ ├── base.py
│ │ ├── integration.py
│ │ ├── plugin.py
│ │ ├── python_script.py
│ │ ├── template.py
│ │ └── theme.py
│ ├── switch.py
│ ├── system_health.py
│ ├── types.py
│ ├── update.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── backup.py
│ │ ├── configuration_schema.py
│ │ ├── data.py
│ │ ├── decode.py
│ │ ├── decorator.py
│ │ ├── file_system.py
│ │ ├── filters.py
│ │ ├── github_graphql_query.py
│ │ ├── json.py
│ │ ├── logger.py
│ │ ├── path.py
│ │ ├── queue_manager.py
│ │ ├── regex.py
│ │ ├── store.py
│ │ ├── url.py
│ │ ├── validate.py
│ │ ├── version.py
│ │ └── workarounds.py
│ ├── validate/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── archived.py
│ │ ├── base.py
│ │ ├── brands.py
│ │ ├── description.py
│ │ ├── hacsjson.py
│ │ ├── images.py
│ │ ├── information.py
│ │ ├── integration_manifest.py
│ │ ├── issues.py
│ │ ├── manager.py
│ │ └── topics.py
│ └── websocket/
│ ├── __init__.py
│ ├── critical.py
│ ├── repositories.py
│ └── repository.py
├── hacs.json
├── info.md
├── pyproject.toml
├── requirements_action.txt
├── requirements_base.txt
├── requirements_core_min.txt
├── requirements_generate_data.txt
├── requirements_lint.txt
├── requirements_test.txt
├── scripts/
│ ├── __init__.py
│ ├── clear_storage
│ ├── coverage
│ ├── data/
│ │ ├── __init__.py
│ │ ├── common.py
│ │ ├── generate_category_data.py
│ │ └── validate_category_data.py
│ ├── develop
│ ├── install/
│ │ ├── core
│ │ ├── core_dev
│ │ ├── frontend
│ │ ├── pip_packages
│ │ └── uv_packages
│ ├── lgtm.js
│ ├── lint
│ ├── setup
│ ├── snapshot-update
│ ├── test
│ └── update/
│ ├── __init__.py
│ ├── default_repositories.py
│ └── manifest.py
└── tests/
├── __init__.py
├── action/
│ └── test_hacs_action_integration.py
├── common.py
├── conftest.py
├── fixtures/
│ ├── proxy/
│ │ ├── api.github.com/
│ │ │ ├── rate_limit.json
│ │ │ └── repos/
│ │ │ ├── hacs/
│ │ │ │ ├── default/
│ │ │ │ │ └── contents/
│ │ │ │ │ ├── appdaemon.json
│ │ │ │ │ ├── integration.json
│ │ │ │ │ ├── plugin.json
│ │ │ │ │ ├── python_script.json
│ │ │ │ │ ├── template.json
│ │ │ │ │ └── theme.json
│ │ │ │ ├── integration/
│ │ │ │ │ ├── contents/
│ │ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ │ └── hacs/
│ │ │ │ │ │ │ └── manifest.json
│ │ │ │ │ │ └── hacs.json
│ │ │ │ │ └── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── integration.json
│ │ │ └── hacs-test-org/
│ │ │ ├── addon-basic/
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── addon-basic.json
│ │ │ ├── appdaemon-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── apps/
│ │ │ │ │ │ └── example.json
│ │ │ │ │ ├── apps.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── appdaemon-basic.json
│ │ │ ├── integration-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ └── example/
│ │ │ │ │ │ └── manifest.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ ├── releases/
│ │ │ │ │ └── latest.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-basic-custom/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ ├── custom_components/
│ │ │ │ │ │ └── example/
│ │ │ │ │ │ └── manifest.json
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-basic-custom.json
│ │ │ ├── integration-basic.json
│ │ │ ├── integration-invalid/
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── integration-invalid.json
│ │ │ ├── plugin-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── plugin-basic.json
│ │ │ ├── plugin-custom-dist/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── plugin-custom-dist.json
│ │ │ ├── python_script-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── python_script-basic.json
│ │ │ ├── template-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ ├── template-basic.json
│ │ │ ├── theme-basic/
│ │ │ │ ├── branches/
│ │ │ │ │ └── main.json
│ │ │ │ ├── contents/
│ │ │ │ │ └── hacs.json
│ │ │ │ ├── git/
│ │ │ │ │ └── trees/
│ │ │ │ │ ├── 1.0.0.json
│ │ │ │ │ ├── 2.0.0.json
│ │ │ │ │ └── main.json
│ │ │ │ └── releases.json
│ │ │ └── theme-basic.json
│ │ ├── brands.home-assistant.io/
│ │ │ └── domains.json
│ │ ├── data-v2.hacs.xyz/
│ │ │ ├── appdaemon/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── critical/
│ │ │ │ └── data.json
│ │ │ ├── integration/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── netdaemon/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── plugin/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── python_script/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── removed/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ ├── template/
│ │ │ │ ├── data.json
│ │ │ │ └── repositories.json
│ │ │ └── theme/
│ │ │ ├── data.json
│ │ │ └── repositories.json
│ │ ├── github.com/
│ │ │ └── hacs-test-org/
│ │ │ ├── appdaemon-basic/
│ │ │ │ └── _base/
│ │ │ │ ├── README.md
│ │ │ │ └── apps/
│ │ │ │ └── example/
│ │ │ │ └── __init__.py
│ │ │ └── integration-basic/
│ │ │ └── _base/
│ │ │ ├── README.md
│ │ │ └── custom_components/
│ │ │ └── example/
│ │ │ └── manifest.json
│ │ └── raw.githubusercontent.com/
│ │ └── hacs-test-org/
│ │ ├── appdaemon-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ └── 2.0.0/
│ │ │ ├── README.md
│ │ │ └── hacs.json
│ │ ├── integration-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ ├── 2.0.0/
│ │ │ │ ├── README.md
│ │ │ │ └── hacs.json
│ │ │ └── main/
│ │ │ └── hacs.json
│ │ ├── plugin-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── hacs.json
│ │ │ │ └── plugin-basic.js
│ │ │ └── 2.0.0/
│ │ │ ├── hacs.json
│ │ │ └── plugin-basic.js
│ │ ├── python_script-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── README.md
│ │ │ │ ├── hacs.json
│ │ │ │ └── python_scripts/
│ │ │ │ └── example.py
│ │ │ └── 2.0.0/
│ │ │ ├── README.md
│ │ │ ├── hacs.json
│ │ │ └── python_scripts/
│ │ │ └── example.py
│ │ ├── template-basic/
│ │ │ ├── 1.0.0/
│ │ │ │ ├── example.jinja
│ │ │ │ └── hacs.json
│ │ │ └── 2.0.0/
│ │ │ ├── example.jinja
│ │ │ └── hacs.json
│ │ └── theme-basic/
│ │ ├── 1.0.0/
│ │ │ ├── hacs.json
│ │ │ └── themes/
│ │ │ └── example.yaml
│ │ └── 2.0.0/
│ │ ├── hacs.json
│ │ └── themes/
│ │ └── example.yaml
│ ├── repository_data.json
│ ├── stored_repositories.json
│ ├── v2-appdaemon-data.json
│ ├── v2-critical-data.json
│ ├── v2-integration-data.json
│ ├── v2-plugin-data.json
│ ├── v2-python_script-data.json
│ ├── v2-removed-data.json
│ ├── v2-template-data.json
│ └── v2-theme-data.json
├── hacsbase/
│ ├── test_backup.py
│ ├── test_configuration.py
│ ├── test_hacs.py
│ └── test_hacsbase_data.py
├── helpers/
│ ├── classes/
│ │ ├── test_repository_data.py
│ │ └── test_validate_class.py
│ ├── download/
│ │ ├── test_gather_files_to_download.py
│ │ └── test_should_try_releases.py
│ ├── filters/
│ │ ├── test_filter_content_return_one_of_type.py
│ │ └── test_get_first_directory_in_directory.py
│ └── functions/
│ └── test_extract_repository_from_url.py
├── homeassistantfixtures/
│ ├── __init__.py
│ ├── common.py
│ ├── dev.py
│ └── min.py
├── integration/
│ └── test_integration_setup.py
├── patch_time.py
├── repositories/
│ ├── helpers/
│ │ ├── __init__.py
│ │ └── test_properties.py
│ ├── test_can_install.py
│ ├── test_display_status.py
│ ├── test_download_repository.py
│ ├── test_get_documentation.py
│ ├── test_get_hacs_json.py
│ ├── test_get_hacs_json_raw.py
│ ├── test_get_reposiotry_releases.py
│ ├── test_hacs_manifest.py
│ ├── test_plugin_repository.py
│ ├── test_register_repository.py
│ ├── test_remove_repository.py
│ ├── test_removed_repository.py
│ └── test_update_repository.py
├── ruff.toml
├── scripts/
│ └── data/
│ └── test_generate_category_data.py
├── snapshots/
│ ├── action/
│ │ └── test_hacs_action_integration/
│ │ ├── bad_documentation.log
│ │ ├── bad_issue_tracker.log
│ │ ├── no_releases.log
│ │ ├── releases_without_assets.log
│ │ └── valid_manifest.log
│ ├── api-usage/
│ │ └── tests/
│ │ ├── action/
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-bad-documentation.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-bad-issue-tracker.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-no-releases.json
│ │ │ ├── test_hacs_action_integrationtest-hacs-action-integration-releases-without-assets.json
│ │ │ └── test_hacs_action_integrationtest-hacs-action-integration-valid-manifest.json
│ │ ├── hacsbase/
│ │ │ ├── test_backuptest-directory.json
│ │ │ ├── test_backuptest-file.json
│ │ │ ├── test_backuptest-muilti.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-async-write1.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-async-write2.json
│ │ │ ├── test_hacsbase_datatest-hacs-data-restore-write-not-new.json
│ │ │ ├── test_hacstest-add-remove-repository.json
│ │ │ └── test_hacstest-hacs.json
│ │ ├── helpers/
│ │ │ └── download/
│ │ │ ├── test_gather_files_to_downloadtest-gather-appdaemon-files-base.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-appdaemon-files-with-subdir.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-content-in-root-theme.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-files-to-download.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-different-card-name.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-dist.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-release-multiple.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-release.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-files-from-root.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-multiple-files-in-root.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-plugin-multiple-plugin-files-from-dist.json
│ │ │ ├── test_gather_files_to_downloadtest-gather-zip-release.json
│ │ │ ├── test_gather_files_to_downloadtest-single-file-repo.json
│ │ │ ├── test_should_try_releasestest-base.json
│ │ │ ├── test_should_try_releasestest-category-is-wrong.json
│ │ │ ├── test_should_try_releasestest-no-releases.json
│ │ │ ├── test_should_try_releasestest-ref-is-default.json
│ │ │ └── test_should_try_releasestest-zip-release.json
│ │ ├── integration/
│ │ │ └── test_integration_setuptest-integration-setup.json
│ │ ├── repositories/
│ │ │ ├── helpers/
│ │ │ │ ├── test_propertiestest-repository-helpers-properties-can-be-installed.json
│ │ │ │ └── test_propertiestest-repository-helpers-properties-pending-update.json
│ │ │ ├── test_can_installtest-hacs-can-install.json
│ │ │ ├── test_display_statustest-display-status.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_download_repositorytest-download-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data0.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data1.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data2.json
│ │ │ ├── test_get_documentationtest-repository-get-documentation-data3.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-1-0-0-expected0.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-99-99-99-none.json
│ │ │ ├── test_get_hacs_json_rawtest-get-hacs-json-raw-with-exception.json
│ │ │ ├── test_get_hacs_jsontest-get-hacs-json-with-exception.json
│ │ │ ├── test_get_hacs_jsontest-validate-repository-1-0-0-integration-basic-1-0-0.json
│ │ │ ├── test_get_hacs_jsontest-validate-repository-99-99-99-none.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-integration-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-plugin-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-python-script-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-template-basic.json
│ │ │ ├── test_get_reposiotry_releasestest-get-reposiotry-releases-hacs-test-org-theme-basic.json
│ │ │ ├── test_plugin_repositorytest-add-dashboard-resource-with-invalid-file-name.json
│ │ │ ├── test_plugin_repositorytest-add-dashboard-resource.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-1-0-0-none-none-100.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-1-7-dev09-r2-none-none-17092.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-2-0-1-none-201.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-none-3-4-2-342.json
│ │ │ ├── test_plugin_repositorytest-dashboard-hacstag-none-none-none.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-awesome-plugin-hacsfiles-awesome-plugin.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-plugin-advanced-hacsfiles-plugin-advanced.json
│ │ │ ├── test_plugin_repositorytest-dashboard-namespace-hacs-test-org-plugin-basic-hacsfiles-plugin-basic.json
│ │ │ ├── test_plugin_repositorytest-dashboard-url.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-hass-data.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-lovelace-data.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-lovelace-resources.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-no-store.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-none-store.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-wrong-key.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler-wrong-version.json
│ │ │ ├── test_plugin_repositorytest-get-resource-handler.json
│ │ │ ├── test_plugin_repositorytest-remove-dashboard-resource.json
│ │ │ ├── test_plugin_repositorytest-update-dashboard-resource.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hacs-test-org-addon-basic-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hacs-test-org-integration-invalid-integration-hacs-test-org-integration-invalid-repository-structure-for-main-is-not-compliant.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-hassio-addons-example-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-home-assistant-addons-the-repository-does-not-seem-to-be-a-integration-but-an-add-on-repository-hacs-does-not-manage-add-ons.json
│ │ │ ├── test_register_repositorytest-register-repository-failures-home-assistant-core-you-can-not-add-homeassistant-core-to-use-core-integrations-check-the-home-assistant-documentation-for-how-to-add-them.json
│ │ │ ├── test_register_repositorytest-register-repository-hacs-test-org-integration-basic-custom-integration.json
│ │ │ ├── test_register_repositorytest-register-repository-hacs-test-org-plugin-custom-dist-plugin.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_remove_repositorytest-remove-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-download-failure.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-integration-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-plugin-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-python-script-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-template-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-hacs-test-org-theme-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-no-manifest.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-no-update.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-old-core-version.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-old-hacs-version.json
│ │ │ ├── test_update_repositorytest-update-repository-entity-same-provided-version.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-integration-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-plugin-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-python-script-basic.json
│ │ │ ├── test_update_repositorytest-update-repository-websocket-hacs-test-org-template-basic.json
│ │ │ └── test_update_repositorytest-update-repository-websocket-hacs-test-org-theme-basic.json
│ │ ├── scripts/
│ │ │ └── data/
│ │ │ ├── test_generate_category_datatest-generate-category-data-error-status-release-304-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-error-status-release-404-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-cancellederror-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-error2-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-errors-release-timeouterror-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-template-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-hacs-test-org-theme-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-template-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-single-repository-hacs-test-org-theme-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-30plus-prereleases-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-appdaemon-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-integration-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-plugin-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-python-script-basic.json
│ │ │ ├── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-template-basic.json
│ │ │ └── test_generate_category_datatest-generate-category-data-with-prior-content-hacs-test-org-theme-basic.json
│ │ ├── test_config_flowtest-flow-with-activation-failure.json
│ │ ├── test_config_flowtest-flow-with-registration-failure.json
│ │ ├── test_config_flowtest-flow-with-remove-while-activating.json
│ │ ├── test_config_flowtest-full-user-flow-implementation.json
│ │ ├── test_config_flowtest-options-flow.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-appdaemon-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-integration-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-plugin-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-python-script-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-template-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-hacs-test-org-theme-basic.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-appdaemon-data0.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-critical-data6.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-integration-data1.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-plugin-data2.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-python-script-data3.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-removed-data7.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-template-data4.json
│ │ ├── test_data_clienttest-basic-functionality-data-validate-theme-data5.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-appdaemon-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-integration-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-plugin-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-python-script-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-template-basic.json
│ │ ├── test_data_clienttest-basic-functionality-repositories-hacs-test-org-theme-basic.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-appdaemon-data0.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-integration-data1.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-plugin-data2.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-python-script-data3.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-template-data4.json
│ │ ├── test_data_clienttest-discard-invalid-repo-data-theme-data5.json
│ │ ├── test_data_clienttest-exception-handling-exception-error-fetching-data-from-hacs-test.json
│ │ ├── test_data_clienttest-exception-handling-timeouterror-timeout-of-60s-reached.json
│ │ ├── test_data_clienttest-status-handling-1009-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-200-does-not-raise.json
│ │ ├── test_data_clienttest-status-handling-201-does-not-raise.json
│ │ ├── test_data_clienttest-status-handling-301-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-302-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-304-hacsnotmodifiedexception.json
│ │ ├── test_data_clienttest-status-handling-400-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-401-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-403-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-418-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-429-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-500-hacsexception.json
│ │ ├── test_data_clienttest-status-handling-529-hacsexception.json
│ │ ├── test_diagnosticstest-diagnostics-with-exception.json
│ │ ├── test_diagnosticstest-diagnostics.json
│ │ ├── test_sensor_cleanuptest-sensor-cleanup.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-appdaemon-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-integration-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-plugin-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-python-script-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-template-basic.json
│ │ ├── test_switchtest-switch-entity-state-hacs-test-org-theme-basic.json
│ │ ├── test_system_healthtest-system-health-after-unload.json
│ │ ├── test_system_healthtest-system-health.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-appdaemon-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-integration-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-plugin-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-python-script-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-template-basic.json
│ │ ├── test_updatetest-update-entity-state-hacs-test-org-theme-basic.json
│ │ ├── utils/
│ │ │ ├── test_pathtest-is-safe.json
│ │ │ ├── test_queue_managertest-queue-manager.json
│ │ │ └── test_versiontest-version-to-download.json
│ │ └── validate/
│ │ ├── test_async_run_repository_checkstest-async-run-repository-checks.json
│ │ ├── test_brands_checktest-added-to-brands.json
│ │ ├── test_brands_checktest-local-brands-asset-content-in-root.json
│ │ ├── test_brands_checktest-local-brands-asset-missing-falls-back-to-remote.json
│ │ ├── test_brands_checktest-local-brands-asset-not-in-root.json
│ │ ├── test_brands_checktest-not-added-to-brands.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-integration-zip-release-with-filename.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-no-manifest.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-invalid-manifest.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-missing-filename.json
│ │ ├── test_hacsjson_checktest-hacs-manifest-with-valid-manifest.json
│ │ ├── test_images_checktest-repository-has-images.json
│ │ ├── test_images_checktest-repository-has-not-images.json
│ │ ├── test_integration_manifest_checktest-hacs-manifest-with-invalid-manifest.json
│ │ ├── test_integration_manifest_checktest-integration-manifest-with-valid-manifest.json
│ │ ├── test_integration_manifest_checktest-integration-no-manifest.json
│ │ ├── test_repository_archived_checktest-repository-archived.json
│ │ ├── test_repository_archived_checktest-repository-not-archived.json
│ │ ├── test_repository_description_checktest-repository-hacs-description.json
│ │ ├── test_repository_description_checktest-repository-no-description.json
│ │ ├── test_repository_information_file_checktest-has-info-file.json
│ │ ├── test_repository_information_file_checktest-has-info-md-file.json
│ │ ├── test_repository_information_file_checktest-has-readme-file.json
│ │ ├── test_repository_information_file_checktest-has-readme-md-file.json
│ │ ├── test_repository_information_file_checktest-no-info-file.json
│ │ ├── test_repository_information_file_checktest-no-readme-file.json
│ │ ├── test_repository_issues_checktest-repository-issues-enabled.json
│ │ ├── test_repository_issues_checktest-repository-issues-not-enabled.json
│ │ ├── test_repository_topics_checktest-repository-hacs-topics.json
│ │ └── test_repository_topics_checktest-repository-no-topics.json
│ ├── config_flow/
│ │ ├── test_already_configured.json
│ │ ├── test_flow_with_activation_failure.json
│ │ ├── test_flow_with_registration_failure.json
│ │ └── test_full_user_flow_implementation.json
│ ├── data_client/
│ │ └── base/
│ │ ├── data/
│ │ │ ├── appdaemon.json
│ │ │ ├── integration.json
│ │ │ ├── plugin.json
│ │ │ ├── python_script.json
│ │ │ ├── template.json
│ │ │ └── theme.json
│ │ ├── data_validate/
│ │ │ ├── appdaemon.json
│ │ │ ├── critical.json
│ │ │ ├── integration.json
│ │ │ ├── plugin.json
│ │ │ ├── python_script.json
│ │ │ ├── removed.json
│ │ │ ├── template.json
│ │ │ └── theme.json
│ │ └── repositories/
│ │ ├── appdaemon.json
│ │ ├── integration.json
│ │ ├── plugin.json
│ │ ├── python_script.json
│ │ ├── template.json
│ │ └── theme.json
│ ├── diagnostics/
│ │ ├── base.json
│ │ └── exception.json
│ ├── hacs-test-org/
│ │ ├── appdaemon-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── integration-basic/
│ │ │ ├── get_documentation/
│ │ │ │ ├── installed_false_last_version_2_0_0.md
│ │ │ │ ├── installed_false_last_version_99_99_99.md
│ │ │ │ ├── installed_true_installed_version_1_0_0.md
│ │ │ │ └── installed_true_installed_version_1_0_0_last_version_2_0_0.md
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── integration-basic-custom/
│ │ │ └── test_register_repository.json
│ │ ├── plugin-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── plugin-custom-dist/
│ │ │ └── test_register_repository.json
│ │ ├── python_script-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ ├── template-basic/
│ │ │ ├── test_discard_invalid_repo_data.json
│ │ │ ├── test_download_repository.json
│ │ │ ├── test_get_reposiotry_releases.json
│ │ │ ├── test_remove_repository_post.json
│ │ │ ├── test_remove_repository_pre.json
│ │ │ ├── test_switch/
│ │ │ │ └── entity_states.json
│ │ │ ├── test_update_entity_state.json
│ │ │ ├── test_update_repository_entity.json
│ │ │ └── test_update_repository_websocket.json
│ │ └── theme-basic/
│ │ ├── test_discard_invalid_repo_data.json
│ │ ├── test_download_repository.json
│ │ ├── test_get_reposiotry_releases.json
│ │ ├── test_remove_repository_post.json
│ │ ├── test_remove_repository_pre.json
│ │ ├── test_switch/
│ │ │ └── entity_states.json
│ │ ├── test_update_entity_state.json
│ │ ├── test_update_repository_entity.json
│ │ └── test_update_repository_websocket.json
│ ├── scripts/
│ │ └── data/
│ │ ├── generate_category_data/
│ │ │ ├── appdaemon/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── integration/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── plugin/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── python_script/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── single/
│ │ │ │ ├── appdaemon/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── appdaemon-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── integration/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── integration-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── plugin/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── plugin-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── python_script/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── python_script-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ ├── template/
│ │ │ │ │ └── hacs-test-org/
│ │ │ │ │ └── template-basic/
│ │ │ │ │ ├── data.json
│ │ │ │ │ ├── repositories.json
│ │ │ │ │ └── summary.json
│ │ │ │ └── theme/
│ │ │ │ └── hacs-test-org/
│ │ │ │ └── theme-basic/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ ├── template/
│ │ │ │ ├── data.json
│ │ │ │ ├── repositories.json
│ │ │ │ └── summary.json
│ │ │ └── theme/
│ │ │ ├── data.json
│ │ │ ├── repositories.json
│ │ │ └── summary.json
│ │ ├── test_generate_category_data_error_status_release/
│ │ │ └── integration/
│ │ │ ├── 304.json
│ │ │ └── 404.json
│ │ ├── test_generate_category_data_errors_release/
│ │ │ └── integration/
│ │ │ ├── CancelledError.json
│ │ │ ├── TimeoutError.json
│ │ │ └── error2.json
│ │ ├── test_generate_category_data_with_30plus_prereleases/
│ │ │ └── integration.json
│ │ └── test_generate_category_data_with_prior_content/
│ │ ├── appdaemon.json
│ │ ├── integration.json
│ │ ├── plugin.json
│ │ ├── python_script.json
│ │ ├── template.json
│ │ └── theme.json
│ ├── system_health/
│ │ ├── system_health.json
│ │ └── system_health_after_unload.json
│ ├── test_integration_setup.json
│ └── test_integration_setup_with_custom_updater.json
├── test_config_flow.py
├── test_data_client.py
├── test_diagnostics.py
├── test_emuns.py
├── test_sensor_cleanup.py
├── test_switch.py
├── test_system_health.py
├── test_update.py
├── utils/
│ ├── test_decorator.py
│ ├── test_fs_util.py
│ ├── test_path.py
│ ├── test_queue_manager.py
│ ├── test_store.py
│ ├── test_url.py
│ ├── test_validate.py
│ ├── test_version.py
│ └── test_workarounds.py
└── validate/
├── test_async_run_repository_checks.py
├── test_brands_check.py
├── test_hacsjson_check.py
├── test_images_check.py
├── test_integration_manifest_check.py
├── test_repository_archived_check.py
├── test_repository_description_check.py
├── test_repository_information_file_check.py
├── test_repository_issues_check.py
└── test_repository_topics_check.py
SYMBOL INDEX (655 symbols across 119 files)
FILE: action/action.py
function error (line 50) | def error(error: str):
function output_in_group (line 55) | def output_in_group(group: str, content: str):
function get_event_data (line 61) | def get_event_data():
function choose_repository (line 68) | async def choose_repository(githubapi: GitHubAPI, category: str):
function choose_category (line 88) | def choose_category():
function preflight (line 94) | async def preflight():
function validate_repository (line 163) | async def validate_repository(hacs: HacsBase, repository: str, category:...
FILE: custom_components/hacs/__init__.py
function _async_initialize_integration (line 36) | async def _async_initialize_integration(
function async_setup_entry (line 182) | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEnt...
function async_unload_entry (line 190) | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEn...
function async_reload_entry (line 225) | async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEn...
FILE: custom_components/hacs/base.py
class RemovedRepository (line 77) | class RemovedRepository:
method update_data (line 86) | def update_data(self, data: dict):
method to_json (line 99) | def to_json(self):
class HacsConfiguration (line 111) | class HacsConfiguration:
method to_json (line 133) | def to_json(self) -> str:
method update_from_dict (line 137) | def update_from_dict(self, data: dict) -> None:
class HacsCore (line 149) | class HacsCore:
class HacsCommon (line 158) | class HacsCommon:
class HacsStatus (line 169) | class HacsStatus:
class HacsSystem (line 180) | class HacsSystem:
method disabled (line 190) | def disabled(self) -> bool:
class HacsRepositories (line 196) | class HacsRepositories:
method list_all (line 206) | def list_all(self) -> list[HacsRepository]:
method list_removed (line 211) | def list_removed(self) -> list[RemovedRepository]:
method list_downloaded (line 216) | def list_downloaded(self) -> list[HacsRepository]:
method category_downloaded (line 220) | def category_downloaded(self, category: HacsCategory) -> bool:
method register (line 227) | def register(self, repository: HacsRepository, default: bool = False) ...
method unregister (line 253) | def unregister(self, repository: HacsRepository) -> None:
method mark_default (line 272) | def mark_default(self, repository: HacsRepository) -> None:
method set_repository_id (line 284) | def set_repository_id(self, repository: HacsRepository, repo_id: str):
method is_default (line 297) | def is_default(self, repository_id: str | None = None) -> bool:
method is_registered (line 303) | def is_registered(
method is_downloaded (line 315) | def is_downloaded(
method get_by_id (line 329) | def get_by_id(self, repository_id: str | None) -> HacsRepository | None:
method get_by_full_name (line 335) | def get_by_full_name(self, repository_full_name: str | None) -> HacsRe...
method is_removed (line 341) | def is_removed(self, repository_full_name: str) -> bool:
method removed_repository (line 345) | def removed_repository(self, repository_full_name: str) -> RemovedRepo...
class HacsBase (line 355) | class HacsBase:
method __init__ (line 372) | def __init__(self) -> None:
method integration_dir (line 385) | def integration_dir(self) -> pathlib.Path:
method set_stage (line 389) | def set_stage(self, stage: HacsStage | None) -> None:
method disable_hacs (line 399) | def disable_hacs(self, reason: HacsDisabledReason) -> None:
method enable_hacs (line 411) | def enable_hacs(self) -> None:
method enable_hacs_category (line 417) | def enable_hacs_category(self, category: HacsCategory) -> None:
method disable_hacs_category (line 424) | def disable_hacs_category(self, category: HacsCategory) -> None:
method async_save_file (line 431) | async def async_save_file(self, file_path: str, content: Any) -> bool:
method async_can_update (line 470) | async def async_can_update(self) -> int:
method async_github_api_method (line 491) | async def async_github_api_method(
method async_register_repository (line 524) | async def async_register_repository(
method startup_tasks (line 606) | async def startup_tasks(self, _=None) -> None:
method async_download_file (line 676) | async def async_download_file(
method async_recreate_entities (line 750) | async def async_recreate_entities(self) -> None:
method async_dispatch (line 775) | def async_dispatch(self, signal: HacsDispatchEvent, data: dict | None ...
method set_active_categories (line 779) | def set_active_categories(self) -> None:
method async_load_hacs_from_github (line 799) | async def async_load_hacs_from_github(self, _=None) -> None:
method async_get_all_category_repositories (line 840) | async def async_get_all_category_repositories(self, _=None) -> None:
method async_get_category_repositories_experimental (line 852) | async def async_get_category_repositories_experimental(self, category:...
method async_check_rate_limit (line 904) | async def async_check_rate_limit(self, _=None) -> None:
method async_process_queue (line 916) | async def async_process_queue(self, _=None) -> None:
method async_handle_removed_repositories (line 948) | async def async_handle_removed_repositories(self, _=None) -> None:
method async_update_downloaded_custom_repositories (line 999) | async def async_update_downloaded_custom_repositories(self, _=None) ->...
method async_handle_critical_repositories (line 1039) | async def async_handle_critical_repositories(self, _=None) -> None:
method async_setup_frontend_endpoint_plugin (line 1101) | async def async_setup_frontend_endpoint_plugin(self) -> None:
FILE: custom_components/hacs/config_flow.py
class HacsFlowHandler (line 39) | class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
method __init__ (line 52) | def __init__(self) -> None:
method async_step_user (line 57) | async def async_step_user(self, user_input):
method async_step_device (line 77) | async def async_step_device(self, _user_input):
method _show_config_form (line 124) | async def _show_config_form(self, user_input):
method async_step_device_done (line 150) | async def async_step_device_done(self, user_input: dict[str, bool] | N...
method async_step_could_not_register (line 170) | async def async_step_could_not_register(self, _user_input=None):
method async_step_reauth (line 174) | async def async_step_reauth(self, _user_input=None):
method async_step_reauth_confirm (line 178) | async def async_step_reauth_confirm(self, user_input=None):
method async_get_options_flow (line 190) | def async_get_options_flow(config_entry):
class HacsOptionsFlowHandler (line 194) | class HacsOptionsFlowHandler(OptionsFlow):
method __init__ (line 197) | def __init__(self, config_entry):
method async_step_init (line 202) | async def async_step_init(self, _user_input=None):
method async_step_user (line 206) | async def async_step_user(self, user_input=None):
FILE: custom_components/hacs/coordinator.py
class HacsUpdateCoordinator (line 12) | class HacsUpdateCoordinator(BaseDataUpdateCoordinatorProtocol):
method __init__ (line 15) | def __init__(self) -> None:
method async_add_listener (line 20) | def async_add_listener(
method async_update_listeners (line 35) | def async_update_listeners(self) -> None:
FILE: custom_components/hacs/data_client.py
class HacsDataClient (line 25) | class HacsDataClient:
method __init__ (line 28) | def __init__(self, session: ClientSession, client_name: str) -> None:
method _do_request (line 34) | async def _do_request(
method get_data (line 64) | async def get_data(self, section: str | None, *, validate: bool) -> di...
method get_repositories (line 96) | async def get_repositories(self, section: str) -> list[str]:
FILE: custom_components/hacs/diagnostics.py
function async_get_config_entry_diagnostics (line 16) | async def async_get_config_entry_diagnostics(
FILE: custom_components/hacs/entity.py
function system_info (line 22) | def system_info(hacs: HacsBase) -> dict:
class HacsBaseEntity (line 35) | class HacsBaseEntity(Entity):
method __init__ (line 41) | def __init__(self, hacs: HacsBase) -> None:
class HacsDispatcherEntity (line 46) | class HacsDispatcherEntity(HacsBaseEntity):
method async_added_to_hass (line 49) | async def async_added_to_hass(self) -> None:
method _update (line 60) | def _update(self) -> None:
method async_update (line 63) | async def async_update(self) -> None:
method _update_and_write_state (line 68) | def _update_and_write_state(self, _: Any) -> None:
class HacsSystemEntity (line 74) | class HacsSystemEntity(HacsDispatcherEntity):
method device_info (line 81) | def device_info(self) -> dict[str, any]:
class HacsRepositoryEntity (line 86) | class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator],...
method __init__ (line 89) | def __init__(
method available (line 102) | def available(self) -> bool:
method device_info (line 107) | def device_info(self) -> dict[str, any]:
method _handle_coordinator_update (line 127) | def _handle_coordinator_update(self) -> None:
method async_update (line 139) | async def async_update(self) -> None:
FILE: custom_components/hacs/enums.py
class HacsGitHubRepo (line 7) | class HacsGitHubRepo(StrEnum):
class HacsCategory (line 14) | class HacsCategory(StrEnum):
method __str__ (line 24) | def __str__(self):
class HacsDispatchEvent (line 28) | class HacsDispatchEvent(StrEnum):
class RepositoryFile (line 41) | class RepositoryFile(StrEnum):
class LovelaceMode (line 48) | class LovelaceMode(StrEnum):
class HacsStage (line 57) | class HacsStage(StrEnum):
class HacsDisabledReason (line 65) | class HacsDisabledReason(StrEnum):
FILE: custom_components/hacs/exceptions.py
class HacsException (line 4) | class HacsException(Exception):
class HacsRepositoryArchivedException (line 8) | class HacsRepositoryArchivedException(HacsException):
class HacsNotModifiedException (line 12) | class HacsNotModifiedException(HacsException):
class HacsExpectedException (line 16) | class HacsExpectedException(HacsException):
class HacsRepositoryExistException (line 20) | class HacsRepositoryExistException(HacsException):
class HacsExecutionStillInProgress (line 24) | class HacsExecutionStillInProgress(HacsException):
class AddonRepositoryException (line 28) | class AddonRepositoryException(HacsException):
method __init__ (line 36) | def __init__(self) -> None:
class HomeAssistantCoreRepositoryException (line 40) | class HomeAssistantCoreRepositoryException(HacsException):
method __init__ (line 48) | def __init__(self) -> None:
FILE: custom_components/hacs/frontend.py
function async_register_frontend (line 23) | async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -...
FILE: custom_components/hacs/repairs.py
class RestartRequiredFixFlow (line 17) | class RestartRequiredFixFlow(RepairsFlow):
method __init__ (line 20) | def __init__(self, issue_id: str) -> None:
method async_step_init (line 23) | async def async_step_init(
method async_step_confirm_restart (line 30) | async def async_step_confirm_restart(
function async_create_fix_flow (line 48) | async def async_create_fix_flow(
FILE: custom_components/hacs/repositories/appdaemon.py
class HacsAppdaemonRepository (line 18) | class HacsAppdaemonRepository(HacsRepository):
method __init__ (line 21) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 31) | def localpath(self):
method validate_repository (line 35) | async def validate_repository(self):
method update_repository (line 63) | async def update_repository(self, ignore_issues=False, force=False):
FILE: custom_components/hacs/repositories/base.py
class FileInformation (line 130) | class FileInformation:
method __init__ (line 133) | def __init__(self, url, path, name):
class RepositoryData (line 140) | class RepositoryData:
method name (line 178) | def name(self):
method to_json (line 184) | def to_json(self):
method create_from_dict (line 189) | def create_from_dict(source: dict, action: bool = False) -> Repository...
method update_data (line 195) | def update_data(self, data: dict, action: bool = False) -> None:
class HacsManifest (line 218) | class HacsManifest:
method to_dict (line 233) | def to_dict(self):
method from_dict (line 238) | def from_dict(manifest: dict):
method update_data (line 257) | def update_data(self, data: dict) -> None:
class RepositoryReleases (line 272) | class RepositoryReleases:
class RepositoryPath (line 283) | class RepositoryPath:
class RepositoryContent (line 290) | class RepositoryContent:
class HacsRepository (line 299) | class HacsRepository:
method __init__ (line 302) | def __init__(self, hacs: HacsBase) -> None:
method __str__ (line 323) | def __str__(self) -> str:
method string (line 328) | def string(self) -> str:
method display_name (line 333) | def display_name(self) -> str:
method ignored_by_country_configuration (line 347) | def ignored_by_country_configuration(self) -> bool:
method display_status (line 361) | def display_status(self) -> str:
method display_installed_version (line 376) | def display_installed_version(self) -> str:
method display_available_version (line 388) | def display_available_version(self) -> str:
method display_version_or_commit (line 402) | def display_version_or_commit(self) -> str:
method pending_update (line 411) | def pending_update(self) -> bool:
method can_download (line 433) | def can_download(self) -> bool:
method localpath (line 445) | def localpath(self) -> str | None:
method should_try_releases (line 450) | def should_try_releases(self) -> bool:
method validate_repository (line 464) | async def validate_repository(self) -> None:
method update_repository (line 468) | async def update_repository(self, ignore_issues=False, force=False) ->...
method common_validate (line 471) | async def common_validate(self, ignore_issues: bool = False) -> None:
method common_registration (line 488) | async def common_registration(self) -> None:
method common_update (line 510) | async def common_update(self, ignore_issues=False, force=False, skip_r...
method download_zip_files (line 559) | async def download_zip_files(self, validate: Validate) -> None:
method async_download_zip_file (line 580) | async def async_download_zip_file(
method download_content (line 620) | async def download_content(self, version: string | None = None) -> None:
method download_repository_zip (line 657) | async def download_repository_zip(self):
method async_get_hacs_json (line 714) | async def async_get_hacs_json(self, ref: str = None) -> dict[str, Any]...
method async_get_info_file_contents (line 730) | async def async_get_info_file_contents(self, *, version: str | None = ...
method remove (line 751) | def remove(self) -> None:
method uninstall (line 757) | async def uninstall(self) -> None:
method remove_local_directory (line 781) | async def remove_local_directory(self) -> None:
method async_pre_registration (line 835) | async def async_pre_registration(self) -> None:
method async_registration (line 839) | async def async_registration(self, ref=None) -> None:
method async_post_registration (line 860) | async def async_post_registration(self) -> None:
method async_pre_install (line 866) | async def async_pre_install(self) -> None:
method _async_pre_install (line 869) | async def _async_pre_install(self) -> None:
method async_install (line 875) | async def async_install(self, *, version: str | None = None, **_) -> N...
method async_post_installation (line 895) | async def async_post_installation(self) -> None:
method async_post_uninstall (line 898) | async def async_post_uninstall(self):
method _async_post_uninstall (line 901) | async def _async_post_uninstall(self):
method _async_post_install (line 905) | async def _async_post_install(self) -> None:
method async_install_repository (line 921) | async def async_install_repository(self, *, version: str | None = None...
method async_get_legacy_repository_object (line 1007) | async def async_get_legacy_repository_object(
method update_filenames (line 1020) | def update_filenames(self) -> None:
method get_tree (line 1023) | async def get_tree(self, ref: str):
method get_releases (line 1033) | async def get_releases(self, prerelease=False, returnlimit=5) -> list[...
method common_update_data (line 1048) | async def common_update_data(
method gather_files_to_download (line 1173) | def gather_files_to_download(self) -> list[FileInformation]:
method release_contents (line 1233) | async def release_contents(self, version: str | None = None) -> list[F...
method dowload_repository_content (line 1253) | async def dowload_repository_content(self, content: FileInformation) -...
method async_remove_entity_device (line 1295) | async def async_remove_entity_device(self) -> None:
method version_to_download (line 1305) | def version_to_download(self) -> str:
method get_documentation (line 1326) | async def get_documentation(
method get_hacs_json (line 1367) | async def get_hacs_json(self, *, version: str, **kwargs) -> HacsManife...
method get_hacs_json_raw (line 1374) | async def get_hacs_json_raw(
method _ensure_download_capabilities (line 1389) | async def _ensure_download_capabilities(self, ref: str | None, **kwarg...
method async_download_repository (line 1419) | async def async_download_repository(self, *, ref: str | None = None, *...
method async_get_releases (line 1453) | async def async_get_releases(self, *, first: int = 30) -> list[GitHubR...
FILE: custom_components/hacs/repositories/integration.py
class HacsIntegrationRepository (line 23) | class HacsIntegrationRepository(HacsRepository):
method __init__ (line 26) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 36) | def localpath(self):
method async_post_installation (line 40) | async def async_post_installation(self):
method async_post_uninstall (line 64) | async def async_post_uninstall(self) -> None:
method validate_repository (line 71) | async def validate_repository(self):
method update_repository (line 121) | async def update_repository(self, ignore_issues=False, force=False):
method reload_custom_components (line 165) | async def reload_custom_components(self):
method async_get_integration_manifest (line 172) | async def async_get_integration_manifest(self, ref: str = None) -> dic...
method get_integration_manifest (line 195) | async def get_integration_manifest(self, *, version: str, **kwargs) ->...
FILE: custom_components/hacs/repositories/plugin.py
class HacsPluginRepository (line 22) | class HacsPluginRepository(HacsRepository):
method __init__ (line 25) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 35) | def localpath(self):
method validate_repository (line 39) | async def validate_repository(self):
method async_post_installation (line 62) | async def async_post_installation(self):
method async_post_uninstall (line 67) | async def async_post_uninstall(self):
method update_repository (line 72) | async def update_repository(self, ignore_issues=False, force=False):
method get_package_content (line 100) | async def get_package_content(self):
method update_filenames (line 111) | def update_filenames(self) -> None:
method generate_dashboard_resource_hacstag (line 149) | def generate_dashboard_resource_hacstag(self) -> str:
method generate_dashboard_resource_namespace (line 158) | def generate_dashboard_resource_namespace(self) -> str:
method generate_dashboard_resource_url (line 162) | def generate_dashboard_resource_url(self) -> str:
method _get_resource_handler (line 173) | def _get_resource_handler(self) -> ResourceStorageCollection | None:
method update_dashboard_resources (line 205) | async def update_dashboard_resources(self) -> None:
method remove_dashboard_resources (line 232) | async def remove_dashboard_resources(self) -> None:
FILE: custom_components/hacs/repositories/python_script.py
class HacsPythonScriptRepository (line 16) | class HacsPythonScriptRepository(HacsRepository):
method __init__ (line 21) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 32) | def localpath(self):
method validate_repository (line 36) | async def validate_repository(self):
method async_post_registration (line 62) | async def async_post_registration(self):
method update_repository (line 71) | async def update_repository(self, ignore_issues=False, force=False):
method update_filenames (line 105) | def update_filenames(self) -> None:
FILE: custom_components/hacs/repositories/template.py
class HacsTemplateRepository (line 18) | class HacsTemplateRepository(HacsRepository):
method __init__ (line 21) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 32) | def localpath(self):
method async_post_installation (line 36) | async def async_post_installation(self):
method validate_repository (line 40) | async def validate_repository(self):
method async_post_registration (line 65) | async def async_post_registration(self):
method async_post_uninstall (line 74) | async def async_post_uninstall(self) -> None:
method _reload_custom_templates (line 78) | async def _reload_custom_templates(self) -> None:
method update_repository (line 87) | async def update_repository(self, ignore_issues=False, force=False):
FILE: custom_components/hacs/repositories/theme.py
class HacsThemeRepository (line 18) | class HacsThemeRepository(HacsRepository):
method __init__ (line 21) | def __init__(self, hacs: HacsBase, full_name: str):
method localpath (line 32) | def localpath(self):
method async_post_installation (line 36) | async def async_post_installation(self):
method validate_repository (line 40) | async def validate_repository(self):
method async_post_registration (line 66) | async def async_post_registration(self):
method _reload_frontend_themes (line 75) | async def _reload_frontend_themes(self) -> None:
method async_post_uninstall (line 83) | async def async_post_uninstall(self) -> None:
method update_repository (line 88) | async def update_repository(self, ignore_issues=False, force=False):
method update_filenames (line 113) | def update_filenames(self) -> None:
FILE: custom_components/hacs/switch.py
function async_setup_entry (line 19) | async def async_setup_entry(
class HacsRepositoryPreReleaseSwitchEntity (line 32) | class HacsRepositoryPreReleaseSwitchEntity(HacsRepositoryEntity, SwitchE...
method __init__ (line 39) | def __init__(self, hacs: HacsBase, repository: HacsRepository) -> None:
method is_on (line 45) | def is_on(self) -> bool:
method async_turn_on (line 49) | async def async_turn_on(self, **kwargs: Any) -> None:
method async_turn_off (line 53) | async def async_turn_off(self, **kwargs: Any) -> None:
method _handle_change (line 57) | async def _handle_change(self, value: bool) -> None:
FILE: custom_components/hacs/system_health.py
function async_register (line 17) | def async_register(hass: HomeAssistant, register: system_health.SystemHe...
function system_health_info (line 23) | async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
FILE: custom_components/hacs/types.py
class DownloadableContent (line 6) | class DownloadableContent(TypedDict):
FILE: custom_components/hacs/update.py
function async_setup_entry (line 20) | async def async_setup_entry(
class HacsRepositoryUpdateEntity (line 31) | class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity):
method name (line 42) | def name(self) -> str | None:
method latest_version (line 47) | def latest_version(self) -> str:
method release_url (line 52) | def release_url(self) -> str:
method installed_version (line 59) | def installed_version(self) -> str:
method release_summary (line 64) | def release_summary(self) -> str | None:
method entity_picture (line 71) | def entity_picture(self) -> str | None:
method async_install (line 81) | async def async_install(self, version: str | None, backup: bool, **kwa...
method async_release_notes (line 93) | async def async_release_notes(self) -> str | None:
method async_added_to_hass (line 137) | async def async_added_to_hass(self) -> None:
method _update_download_progress (line 149) | def _update_download_progress(self, data: dict) -> None:
method _update_in_progress (line 156) | def _update_in_progress(self, progress: int | bool) -> None:
FILE: custom_components/hacs/utils/backup.py
class Backup (line 21) | class Backup:
method __init__ (line 24) | def __init__(
method _init_backup_dir (line 44) | def _init_backup_dir(self) -> bool:
method create (line 59) | def create(self) -> None:
method restore (line 83) | def restore(self) -> None:
method cleanup (line 100) | def cleanup(self) -> None:
FILE: custom_components/hacs/utils/data.py
class HacsData (line 58) | class HacsData:
method __init__ (line 61) | def __init__(self, hacs: HacsBase):
method async_force_write (line 67) | async def async_force_write(self, _=None):
method async_write (line 71) | async def async_write(self, force: bool = False) -> None:
method _async_store_content_and_repos (line 91) | async def _async_store_content_and_repos(self, _=None): # bb: ignore
method _async_store_experimental_content_and_repos (line 103) | async def _async_store_experimental_content_and_repos(self, _=None):
method async_store_repository_data (line 114) | def async_store_repository_data(self, repository: HacsRepository) -> d...
method async_store_experimental_repository_data (line 134) | def async_store_experimental_repository_data(self, repository: HacsRep...
method restore (line 156) | async def restore(self):
method register_unknown_repositories (line 235) | async def register_unknown_repositories(
method async_restore_repository (line 259) | def async_restore_repository(self, entry: str, repository_data: dict[s...
FILE: custom_components/hacs/utils/decode.py
function decode_content (line 6) | def decode_content(content: str) -> str:
FILE: custom_components/hacs/utils/decorator.py
function concurrent (line 16) | def concurrent(
function return_none_on_exception (line 46) | def return_none_on_exception(func):
FILE: custom_components/hacs/utils/file_system.py
function async_exists (line 16) | async def async_exists(hass: HomeAssistant, path: FileDescriptorOrPath) ...
function async_remove (line 21) | async def async_remove(
function async_remove_directory (line 33) | async def async_remove_directory(
FILE: custom_components/hacs/utils/filters.py
function filter_content_return_one_of_type (line 8) | def filter_content_return_one_of_type(
function get_first_directory_in_directory (line 39) | def get_first_directory_in_directory(content: list[str | Any], dirname: ...
FILE: custom_components/hacs/utils/path.py
function _get_safe_paths (line 14) | def _get_safe_paths(
function is_safe (line 32) | def is_safe(hacs: HacsBase, path: str | Path) -> bool:
FILE: custom_components/hacs/utils/queue_manager.py
class QueueManager (line 17) | class QueueManager:
method __init__ (line 20) | def __init__(self, hass: HomeAssistant) -> None:
method pending_tasks (line 26) | def pending_tasks(self) -> int:
method has_pending_tasks (line 31) | def has_pending_tasks(self) -> bool:
method clear (line 35) | def clear(self) -> None:
method add (line 39) | def add(self, task: Coroutine) -> None:
method execute (line 43) | async def execute(self, number_of_tasks: int | None = None) -> None:
FILE: custom_components/hacs/utils/regex.py
function extract_repository_from_url (line 12) | def extract_repository_from_url(url: str) -> str | None:
FILE: custom_components/hacs/utils/store.py
class HACSStore (line 14) | class HACSStore(Store):
method load (line 17) | def load(self):
function get_store_key (line 35) | def get_store_key(key):
function _get_store_for_key (line 40) | def _get_store_for_key(hass, key, encoder):
function get_store_for_key (line 45) | def get_store_for_key(hass, key):
function async_load_from_store (line 50) | async def async_load_from_store(hass, key):
function async_save_to_store (line 55) | async def async_save_to_store(hass, key, data):
function async_remove_store (line 75) | async def async_remove_store(hass, key):
FILE: custom_components/hacs/utils/url.py
function github_release_asset (line 9) | def github_release_asset(
function github_archive (line 20) | def github_archive(
FILE: custom_components/hacs/utils/validate.py
class Validate (line 17) | class Validate:
method success (line 23) | def success(self) -> bool:
function _country_validator (line 28) | def _country_validator(values) -> list[str]:
function validate_repo_data (line 75) | def validate_repo_data(schema: dict[str, Any], extra: int) -> Callable[[...
function validate_version (line 104) | def validate_version(data: Any) -> Any:
FILE: custom_components/hacs/utils/version.py
function version_left_higher_then_right (line 15) | def version_left_higher_then_right(left: str, right: str) -> bool | None:
function version_left_higher_or_equal_then_right (line 31) | def version_left_higher_or_equal_then_right(left: str, right: str) -> bool:
FILE: custom_components/hacs/utils/workarounds.py
function async_register_static_path (line 14) | async def async_register_static_path(
function async_register_static_path (line 26) | async def async_register_static_path(
FILE: custom_components/hacs/validate/archived.py
function async_setup_validator (line 11) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 16) | class Validator(ActionValidationBase):
method async_validate (line 22) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/base.py
class ValidationException (line 14) | class ValidationException(HacsException):
class ActionValidationBase (line 18) | class ActionValidationBase:
method __init__ (line 25) | def __init__(self, repository: HacsRepository) -> None:
method slug (line 31) | def slug(self) -> str:
method async_validate (line 35) | async def async_validate(self) -> None:
method execute_validation (line 38) | async def execute_validation(self, *_: Any, **__: Any) -> None:
FILE: custom_components/hacs/validate/brands.py
function async_setup_validator (line 16) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 21) | class Validator(ActionValidationBase):
method async_validate (line 27) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/description.py
function async_setup_validator (line 11) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 16) | class Validator(ActionValidationBase):
method async_validate (line 22) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/hacsjson.py
function async_setup_validator (line 12) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 17) | class Validator(ActionValidationBase):
method async_validate (line 22) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/images.py
function async_setup_validator (line 14) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 19) | class Validator(ActionValidationBase):
method async_validate (line 25) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/information.py
function async_setup_validator (line 11) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 16) | class Validator(ActionValidationBase):
method async_validate (line 21) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/integration_manifest.py
function async_setup_validator (line 17) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 22) | class Validator(ActionValidationBase):
method async_validate (line 29) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/issues.py
function async_setup_validator (line 11) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 16) | class Validator(ActionValidationBase):
method async_validate (line 22) | async def async_validate(self) -> None:
FILE: custom_components/hacs/validate/manager.py
class ValidationManager (line 19) | class ValidationManager:
method __init__ (line 22) | def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None:
method validators (line 29) | def validators(self) -> list[ActionValidationBase]:
method async_load (line 33) | async def async_load(self, repository: HacsRepository) -> None:
method async_run_repository_checks (line 50) | async def async_run_repository_checks(self, repository: HacsRepository...
FILE: custom_components/hacs/validate/topics.py
function async_setup_validator (line 11) | async def async_setup_validator(repository: HacsRepository) -> Validator:
class Validator (line 16) | class Validator(ActionValidationBase):
method async_validate (line 22) | async def async_validate(self) -> None:
FILE: custom_components/hacs/websocket/__init__.py
function async_register_websocket_commands (line 39) | def async_register_websocket_commands(hass: HomeAssistant) -> None:
function hacs_subscribe (line 73) | async def hacs_subscribe(
function hacs_info (line 100) | async def hacs_info(
FILE: custom_components/hacs/websocket/critical.py
function hacs_critical_list (line 24) | async def hacs_critical_list(
function hacs_critical_acknowledge (line 46) | async def hacs_critical_acknowledge(
FILE: custom_components/hacs/websocket/repositories.py
function hacs_repositories_list (line 31) | async def hacs_repositories_list(
function hacs_repositories_clear_new (line 88) | async def hacs_repositories_clear_new(
function hacs_repositories_removed (line 120) | async def hacs_repositories_removed(
function hacs_repositories_add (line 143) | async def hacs_repositories_add(
function hacs_repositories_remove (line 204) | async def hacs_repositories_remove(
FILE: custom_components/hacs/websocket/repository.py
function hacs_repository_info (line 30) | async def hacs_repository_info(
function hacs_repository_ignore (line 109) | async def hacs_repository_ignore(
function hacs_repository_state (line 142) | async def hacs_repository_state(
function hacs_repository_version (line 166) | async def hacs_repository_version(
function hacs_repository_beta (line 196) | async def hacs_repository_beta(
function hacs_repository_download (line 223) | async def hacs_repository_download(
function hacs_repository_remove (line 254) | async def hacs_repository_remove(
function hacs_repository_refresh (line 282) | async def hacs_repository_refresh(
function hacs_repository_release_notes (line 307) | async def hacs_repository_release_notes(
function hacs_repository_releases (line 341) | async def hacs_repository_releases(
FILE: scripts/data/common.py
function expand_and_humanize_error (line 10) | def expand_and_humanize_error(content: dict[str, Any], error: vol.Invali...
function print_error_and_exit (line 19) | def print_error_and_exit(err: str, category: str, target_path: str | Non...
FILE: scripts/data/generate_category_data.py
function jsonprint (line 62) | def jsonprint(data: any):
function dicts_are_equal (line 73) | def dicts_are_equal(a: dict, b: dict, ignore: set[str]) -> bool:
function repository_has_missing_keys (line 84) | def repository_has_missing_keys(
class AdjustedHacsData (line 110) | class AdjustedHacsData(HacsData):
method register_base_data (line 113) | async def register_base_data(
method async_store_repository_data (line 130) | def async_store_repository_data(self, repository: HacsRepository) -> d...
class AdjustedHacs (line 153) | class AdjustedHacs(HacsBase):
method __init__ (line 158) | def __init__(self, session: ClientSession, *, token: str | None = None):
method async_can_update (line 184) | async def async_can_update(self) -> int:
method concurrent_register_repository (line 191) | async def concurrent_register_repository(
method concurrent_update_repository (line 202) | async def concurrent_update_repository(self, repository: HacsRepositor...
method generate_data_for_category (line 301) | async def generate_data_for_category(
method get_category_repositories (line 355) | async def get_category_repositories(
method summarize_data (line 391) | async def summarize_data(
method async_github_get_hacs_default_file (line 443) | async def async_github_get_hacs_default_file(self, filename: str) -> l...
function generate_category_data (line 456) | async def generate_category_data(category: str, repository_name: str = N...
FILE: scripts/data/validate_category_data.py
function validate_category_data (line 18) | async def validate_category_data(category: str, file_path: str) -> None:
FILE: scripts/update/default_repositories.py
function update (line 7) | def update():
FILE: scripts/update/manifest.py
function update_manifest (line 10) | def update_manifest():
FILE: tests/__init__.py
function async_suggest_report_issue_mock (line 11) | def async_suggest_report_issue_mock(*args, **kwargs):
FILE: tests/action/test_hacs_action_integration.py
function test_hacs_action_integration (line 62) | async def test_hacs_action_integration(
FILE: tests/common.py
class CategoryTestData (line 48) | class CategoryTestData(TypedDict):
function category_test_data_parametrized (line 116) | def category_test_data_parametrized(
function current_function_name (line 136) | def current_function_name():
function safe_json_dumps (line 141) | def safe_json_dumps(data: dict | list) -> str:
function recursive_remove_key (line 150) | def recursive_remove_key(data: dict[str, Any], to_remove: Iterable[str])...
function fixture (line 193) | def fixture(filename, asjson=True):
function dummy_repository_base (line 211) | def dummy_repository_base(hacs, repository=None):
function ensure_auth_manager_loaded (line 237) | def ensure_auth_manager_loaded(auth_mgr):
function mock_storage (line 245) | def mock_storage(data=None):
class MockOwner (line 303) | class MockOwner(auth_models.User):
method __init__ (line 306) | def __init__(self):
method create (line 320) | def create(hass: ha.HomeAssistant):
class MockConfigEntry (line 328) | class MockConfigEntry(config_entries.ConfigEntry):
method add_to_hass (line 331) | def add_to_hass(self, hass: ha.HomeAssistant) -> None:
class WSClient (line 336) | class WSClient:
method __init__ (line 341) | def __init__(self, hass: ha.HomeAssistant, token: str) -> None:
method _create_client (line 346) | async def _create_client(self) -> None:
method send_json (line 374) | async def send_json(self, type: str, payload: dict[str, Any]) -> dict[...
method receive_json (line 379) | async def receive_json(self) -> dict[str, Any]:
method send_and_receive_json (line 382) | async def send_and_receive_json(self, type: str, payload: dict[str, An...
class MockedResponse (line 387) | class MockedResponse:
method __init__ (line 388) | def __init__(self, **kwargs) -> None:
method status (line 394) | def status(self):
method url (line 398) | def url(self):
method headers (line 402) | def headers(self):
method read (line 405) | async def read(self, **kwargs):
method json (line 410) | async def json(self, **kwargs):
method text (line 415) | async def text(self, **kwargs):
method raise_for_status (line 420) | def raise_for_status(self) -> None:
class ResponseMocker (line 425) | class ResponseMocker:
method add (line 429) | def add(self, url: str, response: MockedResponse) -> None:
method get (line 432) | def get(self, url: str, *args, **kwargs) -> MockedResponse:
class ProxyClientSession (line 447) | class ProxyClientSession(ClientSession):
method _request (line 450) | async def _request(self, method: str, str_or_url: StrOrURL, *args, **k...
function client_session_proxy (line 498) | async def client_session_proxy(hass: ha.HomeAssistant) -> ClientSession:
function create_config_entry (line 553) | def create_config_entry(
function setup_integration (line 572) | async def setup_integration(hass: ha.HomeAssistant, config_entry: MockCo...
function get_hacs (line 592) | def get_hacs(hass: ha.HomeAssistant) -> HacsBase:
FILE: tests/conftest.py
function time_freezer (line 88) | def time_freezer() -> Generator[freezegun.api.FrozenDateTimeFactory, Non...
function set_request_context (line 94) | def set_request_context(request: pytest.FixtureRequest):
function connection (line 100) | def connection():
function hass_storage (line 106) | def hass_storage():
function mock_zeroconf_resolver (line 113) | def mock_zeroconf_resolver(event_loop) -> Generator[_patch]:
function hass (line 132) | async def hass(time_freezer, event_loop, tmpdir, check_report_issue: None):
function hacs (line 193) | def hacs(hass: HomeAssistant, setup_integration: None) -> HacsBase:
function repository (line 199) | def repository(hacs):
function repository_integration (line 205) | def repository_integration(hacs):
function repository_theme (line 212) | def repository_theme(hacs):
function repository_plugin (line 219) | def repository_plugin(hacs):
function repository_python_script (line 226) | def repository_python_script(hacs):
function repository_template (line 233) | def repository_template(hacs):
function repository_appdaemon (line 240) | def repository_appdaemon(hacs):
class SnapshotFixture (line 246) | class SnapshotFixture(Snapshot):
method assert_hacs_data (line 247) | async def assert_hacs_data(
function snapshots (line 257) | def snapshots(snapshot: Snapshot) -> SnapshotFixture:
function proxy_session (line 352) | async def proxy_session(hass: HomeAssistant) -> Generator:
function ws_client (line 364) | async def ws_client(hass: HomeAssistant) -> WSClient:
function response_mocker (line 388) | def response_mocker() -> ResponseMocker:
function setup_integration (line 396) | async def setup_integration(hass: HomeAssistant, check_report_issue: Non...
function check_report_issue (line 421) | async def check_report_issue() -> None:
function track_api_usage (line 432) | def track_api_usage(snapshots: SnapshotFixture):
FILE: tests/hacsbase/test_backup.py
function test_file (line 8) | def test_file(hacs, tmpdir):
function test_directory (line 25) | def test_directory(hacs, tmpdir):
function test_muilti (line 41) | def test_muilti(hacs, tmpdir):
FILE: tests/hacsbase/test_configuration.py
function test_configuration_and_option (line 9) | def test_configuration_and_option():
function test_ignore_experimental (line 40) | def test_ignore_experimental():
function test_ignore_netdaemon (line 49) | def test_ignore_netdaemon():
function test_edge_update_with_none (line 58) | def test_edge_update_with_none():
FILE: tests/hacsbase/test_hacs.py
function test_hacs (line 8) | async def test_hacs(hacs, repository, tmpdir):
function test_add_remove_repository (line 36) | async def test_add_remove_repository(hacs, repository, tmpdir):
FILE: tests/hacsbase/test_hacsbase_data.py
function test_hacs_data_async_write1 (line 9) | async def test_hacs_data_async_write1(hacs, repository):
function test_hacs_data_async_write2 (line 17) | async def test_hacs_data_async_write2(hacs):
function test_hacs_data_restore_write_not_new (line 24) | async def test_hacs_data_restore_write_not_new(hacs, caplog):
FILE: tests/helpers/classes/test_repository_data.py
function test_guarded (line 4) | def test_guarded():
FILE: tests/helpers/classes/test_validate_class.py
function test_validate (line 4) | def test_validate():
FILE: tests/helpers/download/test_gather_files_to_download.py
function test_gather_files_to_download (line 7) | def test_gather_files_to_download(repository):
function test_gather_plugin_files_from_root (line 18) | def test_gather_plugin_files_from_root(repository_plugin):
function test_gather_plugin_files_from_dist (line 36) | def test_gather_plugin_files_from_dist(repository_plugin):
function test_gather_plugin_multiple_plugin_files_from_dist (line 63) | def test_gather_plugin_multiple_plugin_files_from_dist(repository_plugin):
function test_gather_plugin_files_from_release (line 82) | def test_gather_plugin_files_from_release(repository_plugin):
function test_gather_plugin_files_from_release_multiple (line 92) | def test_gather_plugin_files_from_release_multiple(repository_plugin):
function test_gather_zip_release (line 104) | def test_gather_zip_release(repository_plugin):
function test_single_file_repo (line 116) | def test_single_file_repo(repository):
function test_gather_content_in_root_theme (line 137) | def test_gather_content_in_root_theme(repository_theme):
function test_gather_appdaemon_files_base (line 156) | def test_gather_appdaemon_files_base(repository_appdaemon):
function test_gather_appdaemon_files_with_subdir (line 173) | def test_gather_appdaemon_files_with_subdir(repository_appdaemon):
function test_gather_plugin_multiple_files_in_root (line 203) | def test_gather_plugin_multiple_files_in_root(repository_plugin):
function test_gather_plugin_different_card_name (line 222) | def test_gather_plugin_different_card_name(repository_plugin):
FILE: tests/helpers/download/test_should_try_releases.py
function test_base (line 5) | def test_base(repository):
function test_ref_is_default (line 12) | def test_ref_is_default(repository):
function test_category_is_wrong (line 19) | def test_category_is_wrong(repository):
function test_no_releases (line 26) | def test_no_releases(repository):
function test_zip_release (line 33) | def test_zip_release(repository):
FILE: tests/helpers/filters/test_filter_content_return_one_of_type.py
function test_valid_objects (line 8) | def test_valid_objects():
function test_valid_list (line 29) | def test_valid_list():
FILE: tests/helpers/filters/test_get_first_directory_in_directory.py
function test_valid (line 8) | def test_valid():
function test_not_valid (line 21) | def test_not_valid():
FILE: tests/helpers/functions/test_extract_repository_from_url.py
function test_extract_repository_from_url (line 5) | def test_extract_repository_from_url():
FILE: tests/homeassistantfixtures/common.py
function get_test_config_dir (line 12) | def get_test_config_dir(*add_path):
function ensure_auth_manager_loaded (line 18) | def ensure_auth_manager_loaded(auth_mgr):
class StoreWithoutWriteLoad (line 25) | class StoreWithoutWriteLoad(storage.Store[_T]):
method async_save (line 28) | async def async_save(self, *args: Any, **kwargs: Any) -> None:
method async_save_delay (line 35) | def async_save_delay(self, *args: Any, **kwargs: Any) -> None:
FILE: tests/homeassistantfixtures/dev.py
function async_test_home_assistant (line 45) | async def async_test_home_assistant(
FILE: tests/homeassistantfixtures/min.py
function async_test_home_assistant (line 46) | async def async_test_home_assistant(
FILE: tests/integration/test_integration_setup.py
function test_integration_setup (line 14) | async def test_integration_setup(
function test_integration_setup_with_custom_updater (line 39) | async def test_integration_setup_with_custom_updater(
FILE: tests/patch_time.py
function _utcnow (line 14) | def _utcnow() -> datetime.datetime:
function _monotonic (line 19) | def _monotonic() -> float:
FILE: tests/repositories/helpers/test_properties.py
function test_repository_helpers_properties_can_be_installed (line 8) | def test_repository_helpers_properties_can_be_installed(hacs):
function test_repository_helpers_properties_pending_update (line 13) | def test_repository_helpers_properties_pending_update(hacs):
FILE: tests/repositories/test_can_install.py
function test_hacs_can_install (line 8) | def test_hacs_can_install(hacs):
FILE: tests/repositories/test_display_status.py
function test_display_status (line 8) | def test_display_status(hacs: HacsBase):
FILE: tests/repositories/test_download_repository.py
function test_download_repository (line 16) | async def test_download_repository(
FILE: tests/repositories/test_get_documentation.py
function test_repository_get_documentation (line 27) | async def test_repository_get_documentation(
FILE: tests/repositories/test_get_hacs_json.py
function test_validate_repository (line 12) | async def test_validate_repository(
function test_get_hacs_json_with_exception (line 30) | async def test_get_hacs_json_with_exception(hacs: HacsBase):
FILE: tests/repositories/test_get_hacs_json_raw.py
function test_get_hacs_json_raw (line 15) | async def test_get_hacs_json_raw(
function test_get_hacs_json_raw_with_exception (line 33) | async def test_get_hacs_json_raw_with_exception(hacs: HacsBase):
FILE: tests/repositories/test_get_reposiotry_releases.py
function test_get_reposiotry_releases (line 19) | async def test_get_reposiotry_releases(
FILE: tests/repositories/test_hacs_manifest.py
function test_manifest_structure (line 9) | def test_manifest_structure():
function test_edge_pass_none (line 42) | def test_edge_pass_none():
FILE: tests/repositories/test_plugin_repository.py
function downloaded_plugin_repository (line 14) | async def downloaded_plugin_repository(
function test_dashboard_namespace (line 34) | async def test_dashboard_namespace(
function test_dashboard_hacstag (line 54) | async def test_dashboard_hacstag(
function test_dashboard_url (line 73) | async def test_dashboard_url(downloaded_plugin_repository: HacsPluginRep...
function test_get_resource_handler (line 81) | async def test_get_resource_handler(
function test_get_resource_handler_wrong_version (line 91) | async def test_get_resource_handler_wrong_version(
function test_get_resource_handler_wrong_key (line 108) | async def test_get_resource_handler_wrong_key(
function test_get_resource_handler_none_store (line 126) | async def test_get_resource_handler_none_store(
function test_get_resource_handler_no_store (line 144) | async def test_get_resource_handler_no_store(
function test_get_resource_handler_no_lovelace_resources (line 162) | async def test_get_resource_handler_no_lovelace_resources(
function test_get_resource_handler_no_lovelace_data (line 179) | async def test_get_resource_handler_no_lovelace_data(
function test_get_resource_handler_no_hass_data (line 191) | async def test_get_resource_handler_no_hass_data(
function test_remove_dashboard_resource (line 205) | async def test_remove_dashboard_resource(
function test_add_dashboard_resource (line 230) | async def test_add_dashboard_resource(
function test_update_dashboard_resource (line 250) | async def test_update_dashboard_resource(
function test_add_dashboard_resource_with_invalid_file_name (line 284) | async def test_add_dashboard_resource_with_invalid_file_name(
FILE: tests/repositories/test_register_repository.py
function test_register_repository (line 20) | async def test_register_repository(
function test_register_repository_failures (line 75) | async def test_register_repository_failures(
FILE: tests/repositories/test_remove_repository.py
function test_remove_repository (line 20) | async def test_remove_repository(
FILE: tests/repositories/test_removed_repository.py
function test_removed_repository (line 25) | def test_removed_repository(data: dict[str, any]):
FILE: tests/repositories/test_update_repository.py
function test_update_repository_entity (line 24) | async def test_update_repository_entity(
function test_update_repository_websocket (line 66) | async def test_update_repository_websocket(
function test_update_repository_entity_no_manifest (line 96) | async def test_update_repository_entity_no_manifest(
function test_update_repository_entity_old_core_version (line 139) | async def test_update_repository_entity_old_core_version(
function test_update_repository_entity_old_hacs_version (line 182) | async def test_update_repository_entity_old_hacs_version(
function test_update_repository_entity_download_failure (line 221) | async def test_update_repository_entity_download_failure(
function test_update_repository_entity_same_provided_version (line 269) | async def test_update_repository_entity_same_provided_version(
function test_update_repository_entity_no_update (line 305) | async def test_update_repository_entity_no_update(
FILE: tests/scripts/data/test_generate_category_data.py
function get_generated_category_data (line 33) | def get_generated_category_data(category: str) -> dict[str, Any]:
function test_generate_category_data_single_repository (line 52) | async def test_generate_category_data_single_repository(
function test_generate_category_data (line 95) | async def test_generate_category_data(
function test_generate_category_data_with_prior_content (line 138) | async def test_generate_category_data_with_prior_content(
function test_generate_category_data_errors_release (line 187) | async def test_generate_category_data_errors_release(
function test_generate_category_data_error_status_release (line 216) | async def test_generate_category_data_error_status_release(
function test_generate_category_data_with_30plus_prereleases (line 264) | async def test_generate_category_data_with_30plus_prereleases(
FILE: tests/test_config_flow.py
function _mock_setup_entry (line 28) | def _mock_setup_entry(hass: HomeAssistant) -> Generator[None, None, None]:
function test_full_user_flow_implementation (line 35) | async def test_full_user_flow_implementation(
function test_flow_with_remove_while_activating (line 120) | async def test_flow_with_remove_while_activating(
function test_flow_with_registration_failure (line 187) | async def test_flow_with_registration_failure(
function test_flow_with_activation_failure (line 229) | async def test_flow_with_activation_failure(
function test_already_configured (line 306) | async def test_already_configured(
function test_options_flow (line 330) | async def test_options_flow(hass: HomeAssistant, setup_integration: Gene...
FILE: tests/test_data_client.py
function test_basic_functionality_data (line 27) | async def test_basic_functionality_data(
function test_basic_functionality_repositories (line 42) | async def test_basic_functionality_repositories(
function test_exception_handling (line 71) | async def test_exception_handling(
function test_status_handling (line 109) | async def test_status_handling(
function without (line 148) | def without(d: dict, key: str) -> dict:
function test_basic_functionality_data_validate (line 168) | async def test_basic_functionality_data_validate(
function test_discard_invalid_repo_data (line 205) | async def test_discard_invalid_repo_data(
FILE: tests/test_diagnostics.py
function test_diagnostics (line 19) | async def test_diagnostics(hacs: HacsBase, snapshots: SnapshotFixture):
function test_diagnostics_with_exception (line 33) | async def test_diagnostics_with_exception(
FILE: tests/test_emuns.py
function test_enum_value (line 6) | def test_enum_value():
FILE: tests/test_sensor_cleanup.py
function test_sensor_cleanup (line 11) | async def test_sensor_cleanup(hass: HomeAssistant) -> None:
FILE: tests/test_switch.py
function test_switch_entity_state (line 27) | async def test_switch_entity_state(
FILE: tests/test_system_health.py
function get_system_health_info (line 19) | async def get_system_health_info(hass: HomeAssistant, domain: str) -> di...
function test_system_health (line 24) | async def test_system_health(
function test_system_health_after_unload (line 65) | async def test_system_health_after_unload(
function test_system_health_no_hacs (line 81) | async def test_system_health_no_hacs(
FILE: tests/test_update.py
function test_update_entity_state (line 27) | async def test_update_entity_state(
FILE: tests/utils/test_decorator.py
function test_sync_function_no_exception (line 7) | def test_sync_function_no_exception():
function test_sync_function_with_exception (line 16) | def test_sync_function_with_exception():
function test_sync_method_no_exception (line 25) | def test_sync_method_no_exception():
function test_sync_method_with_exception (line 36) | def test_sync_method_with_exception():
function test_async_function_no_exception (line 48) | async def test_async_function_no_exception():
function test_async_function_with_exception (line 58) | async def test_async_function_with_exception():
function test_async_method_no_exception (line 68) | async def test_async_method_no_exception():
function test_async_method_with_exception (line 80) | async def test_async_method_with_exception():
function test_async_method_with_args (line 92) | async def test_async_method_with_args():
function test_async_method_with_args_exception (line 107) | async def test_async_method_with_args_exception():
FILE: tests/utils/test_fs_util.py
function test_async_exists (line 13) | async def test_async_exists(hass, tmpdir):
function test_async_remove (line 21) | async def test_async_remove(hass, tmpdir):
function test_async_remove_directory (line 43) | async def test_async_remove_directory(hass, tmpdir):
FILE: tests/utils/test_path.py
function test_is_safe (line 5) | def test_is_safe(hacs: HacsBase) -> None:
FILE: tests/utils/test_queue_manager.py
function test_queue_manager (line 13) | async def test_queue_manager(hacs: HacsBase, caplog: pytest.LogCaptureFi...
FILE: tests/utils/test_store.py
function test_store_load (line 17) | async def test_store_load(hass: HomeAssistant) -> None:
function test_store_remove (line 38) | async def test_store_remove(hass: HomeAssistant) -> None:
function test_store_store (line 50) | async def test_store_store(hass: HomeAssistant, caplog: pytest.LogCaptur...
FILE: tests/utils/test_url.py
function test_github_release_asset (line 16) | def test_github_release_asset(arguments: dict[str, str], url: str) -> None:
function test_github_archive (line 50) | def test_github_archive(arguments: dict[str, str], url: str) -> None:
FILE: tests/utils/test_validate.py
function test_hacs_manifest_json_schema (line 22) | def test_hacs_manifest_json_schema():
function test_integration_json_schema (line 92) | def test_integration_json_schema():
function test_critical_repo_data_json_schema (line 110) | def test_critical_repo_data_json_schema():
function test_critical_repo_data_json_schema_bad_data (line 173) | def test_critical_repo_data_json_schema_bad_data(data: dict, expectation...
function test_repo_data_json_schema (line 192) | def test_repo_data_json_schema(category: str):
function without (line 223) | def without(d: dict, key: str) -> dict:
function test_repo_data_json_schema_bad_data (line 591) | def test_repo_data_json_schema_bad_data(
function test_repo_data_json_schema_multiple_bad_data (line 636) | def test_repo_data_json_schema_multiple_bad_data(categories: list[str], ...
function test_removed_repo_data_json_schema (line 666) | def test_removed_repo_data_json_schema():
function test_removed_repo_data_json_schema_bad_data (line 738) | def test_removed_repo_data_json_schema_bad_data(data: dict, expectation_...
FILE: tests/utils/test_version.py
function test_version_to_download (line 7) | def test_version_to_download(repository):
function test_version_left_higher_or_equal_then_right (line 85) | def test_version_left_higher_or_equal_then_right(left: str, right: str, ...
FILE: tests/utils/test_workarounds.py
function test_domain_ovverides (line 4) | def test_domain_ovverides() -> None:
FILE: tests/validate/test_async_run_repository_checks.py
function test_async_run_repository_checks (line 10) | async def test_async_run_repository_checks(
FILE: tests/validate/test_brands_check.py
function test_added_to_brands (line 7) | async def test_added_to_brands(repository, response_mocker: ResponseMock...
function test_not_added_to_brands (line 18) | async def test_not_added_to_brands(repository, response_mocker: Response...
function test_local_brands_asset_content_in_root (line 29) | async def test_local_brands_asset_content_in_root(repository):
function test_local_brands_asset_not_in_root (line 39) | async def test_local_brands_asset_not_in_root(repository):
function test_local_brands_asset_missing_falls_back_to_remote (line 50) | async def test_local_brands_asset_missing_falls_back_to_remote(
FILE: tests/validate/test_hacsjson_check.py
function test_hacs_manifest_no_manifest (line 17) | async def test_hacs_manifest_no_manifest(repository, caplog):
function test_hacs_manifest_with_valid_manifest (line 24) | async def test_hacs_manifest_with_valid_manifest(repository):
function test_hacs_manifest_with_invalid_manifest (line 38) | async def test_hacs_manifest_with_invalid_manifest(repository):
function test_hacs_manifest_with_missing_filename (line 51) | async def test_hacs_manifest_with_missing_filename(repository, caplog):
function test_hacs_manifest_integration_zip_release_with_filename (line 69) | async def test_hacs_manifest_integration_zip_release_with_filename(repos...
FILE: tests/validate/test_images_check.py
function test_repository_has_images (line 4) | async def test_repository_has_images(repository):
function test_repository_has_not_images (line 19) | async def test_repository_has_not_images(repository):
FILE: tests/validate/test_integration_manifest_check.py
function test_integration_no_manifest (line 6) | async def test_integration_no_manifest(repository_integration):
function test_integration_manifest_with_valid_manifest (line 12) | async def test_integration_manifest_with_valid_manifest(repository_integ...
function test_hacs_manifest_with_invalid_manifest (line 36) | async def test_hacs_manifest_with_invalid_manifest(repository_integration):
FILE: tests/validate/test_repository_archived_check.py
function test_repository_archived (line 4) | async def test_repository_archived(repository):
function test_repository_not_archived (line 11) | async def test_repository_not_archived(repository):
FILE: tests/validate/test_repository_description_check.py
function test_repository_no_description (line 4) | async def test_repository_no_description(repository):
function test_repository_hacs_description (line 11) | async def test_repository_hacs_description(repository):
FILE: tests/validate/test_repository_information_file_check.py
function test_no_info_file (line 6) | async def test_no_info_file(repository):
function test_no_readme_file (line 12) | async def test_no_readme_file(repository):
function test_has_info_file (line 18) | async def test_has_info_file(repository):
function test_has_info_md_file (line 27) | async def test_has_info_md_file(repository):
function test_has_readme_file (line 36) | async def test_has_readme_file(repository):
function test_has_readme_md_file (line 46) | async def test_has_readme_md_file(repository):
FILE: tests/validate/test_repository_issues_check.py
function test_repository_issues_enabled (line 4) | async def test_repository_issues_enabled(repository):
function test_repository_issues_not_enabled (line 11) | async def test_repository_issues_not_enabled(repository):
FILE: tests/validate/test_repository_topics_check.py
function test_repository_no_topics (line 4) | async def test_repository_no_topics(repository):
function test_repository_hacs_topics (line 11) | async def test_repository_hacs_topics(repository):
Condensed preview — 701 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,650K chars).
[
{
"path": ".codeclimate.yml",
"chars": 228,
"preview": "---\nengines:\n duplication:\n enabled: true\n config:\n languages:\n - python\n fixme:\n enabled: true\n "
},
{
"path": ".codecov.yml",
"chars": 507,
"preview": "comment: false\ncodecov:\n branch: main\n\ncoverage:\n precision: 2\n round: down\n range: \"60...100\"\n\n status:\n patch:"
},
{
"path": ".coveragerc",
"chars": 158,
"preview": "[run]\nsource = custom_components\n\nomit =\n # omit tests\n tests/*\n\n # omit scripts\n scripts/update/*\n\n[report]"
},
{
"path": ".devcontainer.json",
"chars": 1755,
"preview": "{\n \"name\": \"hacs/integration\",\n \"image\": \"mcr.microsoft.com/devcontainers/python:1-3.13\",\n \"postCreateCommand\": \"scri"
},
{
"path": ".dockerignore",
"chars": 85,
"preview": "*\n!custom_components/hacs\n!scripts\n!action\n!constraints.txt\n!requirements_action.txt\n"
},
{
"path": ".gitattributes",
"chars": 11,
"preview": "text eol=lf"
},
{
"path": ".github/ISSUE_TEMPLATE/a_integration.yml",
"chars": 2791,
"preview": "---\nname: \"Backend/Integration\"\ndescription: You use this when something is not doing what it's supposed to do.\nlabels: "
},
{
"path": ".github/ISSUE_TEMPLATE/b_frontend.yml",
"chars": 3319,
"preview": "---\nname: \"Frontend\"\ndescription: You use this when elements in the UI are not working correctly.\nlabels: \"issue:fronten"
},
{
"path": ".github/ISSUE_TEMPLATE/c_bot.yml",
"chars": 1409,
"preview": "---\nname: \"hacs-bot\"\ndescription: You use this when hacs-bot did something wrong.\nlabels: \"issue:bot\"\nbody:\n- type: mark"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 787,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: HACS Looks different\n url: https://experimental.hacs.xyz/docs/us"
},
{
"path": ".github/ISSUE_TEMPLATE/d_documentation.yml",
"chars": 742,
"preview": "---\nname: \"Documentation\"\ndescription: You use this when something is wrong with the documentation.\nlabels: \"issue:docum"
},
{
"path": ".github/ISSUE_TEMPLATE/e_action.yml",
"chars": 1349,
"preview": "---\nname: \"HACS Action\"\ndescription: You use this when there is an issue with the HACS action.\nlabels: \"issue:action\"\nbo"
},
{
"path": ".github/ISSUE_TEMPLATE/f_addon.yml",
"chars": 1727,
"preview": "---\nname: \"HACS Add-ons\"\ndescription: You use this when there is an issue with one of the HACS Add-ons\nlabels: \"issue:ad"
},
{
"path": ".github/ISSUE_TEMPLATE/removal.yml",
"chars": 1621,
"preview": "---\nname: Request for repository removal\ndescription: Flagging of repository that should be removed from HACS\nlabels: fl"
},
{
"path": ".github/dependabot.yml",
"chars": 585,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"devcontainers\"\n directory: \"/\"\n labels:\n - \"pr: dependency-update"
},
{
"path": ".github/pre-commit-config.yaml",
"chars": 2347,
"preview": "repos:\n - repo: local\n hooks:\n - id: codespell\n name: Check code for common misspellings\n languag"
},
{
"path": ".github/release.yml",
"chars": 531,
"preview": "changelog:\n categories:\n - title: '💥 Breaking changes'\n labels:\n - 'Breaking Change'\n\n - title: '🛎️ E"
},
{
"path": ".github/workflows/action-container.yml",
"chars": 1690,
"preview": "name: \"Build the action container\"\n\non:\n release:\n types:\n - published\n push:\n branches:\n - main\n p"
},
{
"path": ".github/workflows/generate-hacs-data.yml",
"chars": 10975,
"preview": "name: Generate HACS Data\n\non:\n workflow_dispatch:\n inputs:\n forceRepositoryUpdate:\n description: 'Force "
},
{
"path": ".github/workflows/lint.yaml",
"chars": 1658,
"preview": "name: Lint\n\non:\n pull_request:\n branches:\n - main\n push:\n branches:\n - main\n\nconcurrency:\n group: lin"
},
{
"path": ".github/workflows/lock.yml",
"chars": 602,
"preview": "name: \"Lock closed issues and PR's\"\n\non:\n schedule:\n - cron: \"0 * * * *\"\n\nconcurrency:\n group: lock-${{ github.ref "
},
{
"path": ".github/workflows/publish.yml",
"chars": 2131,
"preview": "name: Publish\n\non:\n release:\n types:\n - published\n push:\n branches:\n - main\n\nconcurrency:\n group: pub"
},
{
"path": ".github/workflows/pull_requests_labels.yml",
"chars": 748,
"preview": "name: \"Check Pull Request labels\"\n\non:\n pull_request:\n types:\n - labeled\n - opened\n - synchronize\n "
},
{
"path": ".github/workflows/pytest.yml",
"chars": 2685,
"preview": "name: Test\n\non:\n pull_request:\n branches:\n - main\n push:\n branches:\n - main\n\nconcurrency:\n group: tes"
},
{
"path": ".github/workflows/stale.yml",
"chars": 560,
"preview": "name: 'Close stale issues'\n\non:\n workflow_dispatch:\n schedule:\n - cron: '30 10 * * *'\n\npermissions: {}\n\njobs:\n iss"
},
{
"path": ".github/workflows/validate.yml",
"chars": 6212,
"preview": "name: Validate\n\non:\n pull_request:\n branches:\n - main\n push:\n branches:\n - main\n schedule:\n - cron"
},
{
"path": ".gitignore",
"chars": 388,
"preview": "# artifacts\n__pycache__\n.pytest*\n*.egg-info\n*/build/*\n*/dist/*\n\n\n# misc\n.claude\n.coverage\n.python-version\n.venv\n.vscode\n"
},
{
"path": ".pylintrc",
"chars": 124,
"preview": "[MESSAGES CONTROL]\n# pylint issue with Python 3.9 https://github.com/PyCQA/pylint/issues/3882\ndisable=unsubscriptable-ob"
},
{
"path": "LICENSE",
"chars": 1090,
"preview": "MIT License\n\nCopyright (c) 2019 - 2023 Joakim Sørensen (@ludeeus)\n\nPermission is hereby granted, free of charge, to any "
},
{
"path": "README.md",
"chars": 1044,
"preview": "# HACS (Home Assistant Community Store)\n\n_Manage (Install, track, upgrade) and discover custom elements for Home Assista"
},
{
"path": "action/Dockerfile",
"chars": 938,
"preview": "FROM python:3.13-alpine\nWORKDIR /hacs\n\nCOPY . /hacs\n\nENV \\\n UV_SYSTEM_PYTHON=true \\\n UV_EXTRA_INDEX_URL=\"https://w"
},
{
"path": "action/action.py",
"chars": 6602,
"preview": "\"\"\"Validate a GitHub repository to be used with HACS.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json"
},
{
"path": "constraints.txt",
"chars": 0,
"preview": ""
},
{
"path": "custom_components/hacs/__init__.py",
"chars": 7689,
"preview": "\"\"\"HACS gives you a powerful UI to handle downloads of all your custom needs.\n\nFor more details about this integration, "
},
{
"path": "custom_components/hacs/base.py",
"chars": 41801,
"preview": "\"\"\"Base HACS class.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Awaitable, Callab"
},
{
"path": "custom_components/hacs/config_flow.py",
"chars": 8335,
"preview": "\"\"\"Adds config flow for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom contextlib import suppress\nfro"
},
{
"path": "custom_components/hacs/const.py",
"chars": 3148,
"preview": "\"\"\"Constants for HACS\"\"\"\n\nfrom typing import TypeVar\n\nfrom aiogithubapi.common.const import ACCEPT_HEADERS\n\nNAME_SHORT ="
},
{
"path": "custom_components/hacs/coordinator.py",
"chars": 1177,
"preview": "\"\"\"Coordinator to trigger entity updates.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nf"
},
{
"path": "custom_components/hacs/data_client.py",
"chars": 3335,
"preview": "\"\"\"HACS Data client.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any\n\nfrom aiohttp import "
},
{
"path": "custom_components/hacs/diagnostics.py",
"chars": 2674,
"preview": "\"\"\"Diagnostics support for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom aiogithubapi impor"
},
{
"path": "custom_components/hacs/entity.py",
"chars": 4525,
"preview": "\"\"\"HACS Base entities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom homeassistant"
},
{
"path": "custom_components/hacs/enums.py",
"chars": 1585,
"preview": "\"\"\"Helper constants.\"\"\"\n\n# pylint: disable=missing-class-docstring\nfrom enum import StrEnum\n\n\nclass HacsGitHubRepo(StrEn"
},
{
"path": "custom_components/hacs/exceptions.py",
"chars": 1363,
"preview": "\"\"\"Custom Exceptions for HACS.\"\"\"\n\n\nclass HacsException(Exception):\n \"\"\"Super basic.\"\"\"\n\n\nclass HacsRepositoryArchive"
},
{
"path": "custom_components/hacs/frontend.py",
"chars": 2202,
"preview": "\"\"\"Starting setup task: Frontend.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING\n\nfr"
},
{
"path": "custom_components/hacs/icons.json",
"chars": 173,
"preview": "{\n \"entity\": {\n \"switch\": {\n \"pre-release\": {\n \"state\": {\n \"on\": \"mdi:test-tube\",\n \"of"
},
{
"path": "custom_components/hacs/iconset.js",
"chars": 3842,
"preview": "const hacsIcons = {\n hacs: {\n path: \"m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 "
},
{
"path": "custom_components/hacs/manifest.json",
"chars": 575,
"preview": "{\n \"domain\": \"hacs\",\n \"name\": \"HACS\",\n \"after_dependencies\": [\n \"python_script\"\n ],\n \"codeowners\":"
},
{
"path": "custom_components/hacs/repairs.py",
"chars": 1765,
"preview": "\"\"\"Repairs platform for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom homeassistant import "
},
{
"path": "custom_components/hacs/repositories/__init__.py",
"chars": 786,
"preview": "\"\"\"Initialize repositories.\"\"\"\n\nfrom __future__ import annotations\n\nfrom ..enums import HacsCategory\nfrom .appdaemon imp"
},
{
"path": "custom_components/hacs/repositories/appdaemon.py",
"chars": 3196,
"preview": "\"\"\"Class for appdaemon apps in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom aiog"
},
{
"path": "custom_components/hacs/repositories/base.py",
"chars": 55015,
"preview": "\"\"\"Repository.\"\"\"\n\nfrom __future__ import annotations\n\nfrom asyncio import sleep\nfrom datetime import UTC, datetime\nimpo"
},
{
"path": "custom_components/hacs/repositories/integration.py",
"chars": 8718,
"preview": "\"\"\"Class for integrations in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom h"
},
{
"path": "custom_components/hacs/repositories/plugin.py",
"chars": 9140,
"preview": "\"\"\"Class for plugins in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom ."
},
{
"path": "custom_components/hacs/repositories/python_script.py",
"chars": 3694,
"preview": "\"\"\"Class for python_scripts in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom ..en"
},
{
"path": "custom_components/hacs/repositories/template.py",
"chars": 3628,
"preview": "\"\"\"Class for themes in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom homeassistan"
},
{
"path": "custom_components/hacs/repositories/theme.py",
"chars": 4039,
"preview": "\"\"\"Class for themes in HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom homeassistan"
},
{
"path": "custom_components/hacs/switch.py",
"chars": 2780,
"preview": "\"\"\"Switch entities for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom homeassistant.componen"
},
{
"path": "custom_components/hacs/system_health.py",
"chars": 1925,
"preview": "\"\"\"Provide info to system health.\"\"\"\n\nfrom typing import Any\n\nfrom aiogithubapi.common.const import BASE_API_URL\nfrom ho"
},
{
"path": "custom_components/hacs/types.py",
"chars": 155,
"preview": "\"\"\"Custom HACS types.\"\"\"\n\nfrom typing import TypedDict\n\n\nclass DownloadableContent(TypedDict):\n \"\"\"Downloadable conte"
},
{
"path": "custom_components/hacs/update.py",
"chars": 6352,
"preview": "\"\"\"Update entities for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom homeassistant.componen"
},
{
"path": "custom_components/hacs/utils/__init__.py",
"chars": 29,
"preview": "\"\"\"Initialize HACS utils.\"\"\"\n"
},
{
"path": "custom_components/hacs/utils/backup.py",
"chars": 3568,
"preview": "\"\"\"Backup.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport tempfile\nfrom time import sleep\nfrom t"
},
{
"path": "custom_components/hacs/utils/configuration_schema.py",
"chars": 178,
"preview": "\"\"\"HACS Configuration Schemas.\"\"\"\n\n# Configuration:\nSIDEPANEL_TITLE = \"sidepanel_title\"\nSIDEPANEL_ICON = \"sidepanel_icon"
},
{
"path": "custom_components/hacs/utils/data.py",
"chars": 12957,
"preview": "\"\"\"Data handler for HACS.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import UTC, datetime\nfrom"
},
{
"path": "custom_components/hacs/utils/decode.py",
"chars": 208,
"preview": "\"\"\"Util to decode content from the github API.\"\"\"\n\nfrom base64 import b64decode\n\n\ndef decode_content(content: str) -> st"
},
{
"path": "custom_components/hacs/utils/decorator.py",
"chars": 1849,
"preview": "\"\"\"HACS Decorators.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Coroutine\nfrom fu"
},
{
"path": "custom_components/hacs/utils/file_system.py",
"chars": 1120,
"preview": "\"\"\"File system functions.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport shutil\nfrom typing import TypeAlias\n\n"
},
{
"path": "custom_components/hacs/utils/filters.py",
"chars": 1540,
"preview": "\"\"\"Filter functions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\ndef filter_content_return_one_of_t"
},
{
"path": "custom_components/hacs/utils/github_graphql_query.py",
"chars": 372,
"preview": "\"\"\"GitHub GraphQL Queries.\"\"\"\n\nGET_REPOSITORY_RELEASES = \"\"\"\nquery ($owner: String!, $name: String!, $first: Int!) {\n r"
},
{
"path": "custom_components/hacs/utils/json.py",
"chars": 92,
"preview": "\"\"\"JSON utils.\"\"\"\n\nfrom homeassistant.util.json import json_loads\n\n__all__ = [\"json_loads\"]\n"
},
{
"path": "custom_components/hacs/utils/logger.py",
"chars": 138,
"preview": "\"\"\"Custom logger for HACS.\"\"\"\n\nimport logging\n\nfrom ..const import PACKAGE_NAME\n\nLOGGER: logging.Logger = logging.getLog"
},
{
"path": "custom_components/hacs/utils/path.py",
"chars": 1171,
"preview": "\"\"\"Path utils\"\"\"\n\nfrom __future__ import annotations\n\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typi"
},
{
"path": "custom_components/hacs/utils/queue_manager.py",
"chars": 2469,
"preview": "\"\"\"The QueueManager class.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Coroutine\n"
},
{
"path": "custom_components/hacs/utils/regex.py",
"chars": 404,
"preview": "\"\"\"Regex utils\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\nRE_REPOSITORY = re.compile(\n r\"(?:(?:.*github.com.)"
},
{
"path": "custom_components/hacs/utils/store.py",
"chars": 2585,
"preview": "\"\"\"Storage handers.\"\"\"\n\nfrom homeassistant.helpers.json import JSONEncoder\nfrom homeassistant.helpers.storage import Sto"
},
{
"path": "custom_components/hacs/utils/url.py",
"chars": 746,
"preview": "\"\"\"Various URL utils for HACS.\"\"\"\n\nimport re\nfrom typing import Literal\n\nGIT_SHA = re.compile(r\"^[a-fA-F0-9]{40}$\")\n\n\nde"
},
{
"path": "custom_components/hacs/utils/validate.py",
"chars": 6369,
"preview": "\"\"\"Validation utilities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfrom dataclasses i"
},
{
"path": "custom_components/hacs/utils/version.py",
"chars": 1058,
"preview": "\"\"\"Version utils.\"\"\"\n\nfrom __future__ import annotations\n\nfrom functools import lru_cache\n\nfrom awesomeversion import (\n"
},
{
"path": "custom_components/hacs/utils/workarounds.py",
"chars": 1108,
"preview": "\"\"\"Workarounds.\"\"\"\n\nfrom homeassistant.core import HomeAssistant\n\nDOMAIN_OVERRIDES = {\n # https://github.com/hacs/int"
},
{
"path": "custom_components/hacs/validate/README.md",
"chars": 862,
"preview": "# Repository validation\n\nThis is where the validation rules that run against the various repository categories live.\n\n##"
},
{
"path": "custom_components/hacs/validate/__init__.py",
"chars": 29,
"preview": "\"\"\"Initialize validation.\"\"\"\n"
},
{
"path": "custom_components/hacs/validate/archived.py",
"chars": 718,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom .base import ActionValidationBase, Validation"
},
{
"path": "custom_components/hacs/validate/base.py",
"chars": 1518,
"preview": "\"\"\"Base class for validation.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom ..exce"
},
{
"path": "custom_components/hacs/validate/brands.py",
"chars": 1894,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom custom_components.hacs.enums import HacsCateg"
},
{
"path": "custom_components/hacs/validate/description.py",
"chars": 734,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom .base import ActionValidationBase, Validation"
},
{
"path": "custom_components/hacs/validate/hacsjson.py",
"chars": 1777,
"preview": "from __future__ import annotations\n\nfrom voluptuous.error import Invalid\nfrom voluptuous.humanize import humanize_error\n"
},
{
"path": "custom_components/hacs/validate/images.py",
"chars": 1129,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom ..enums import HacsCategory\nfrom .base import"
},
{
"path": "custom_components/hacs/validate/information.py",
"chars": 958,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom .base import ActionValidationBase, Validation"
},
{
"path": "custom_components/hacs/validate/integration_manifest.py",
"chars": 1582,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom voluptuous.error import Invalid\nfrom voluptuo"
},
{
"path": "custom_components/hacs/validate/issues.py",
"chars": 743,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom .base import ActionValidationBase, Validation"
},
{
"path": "custom_components/hacs/validate/manager.py",
"chars": 2767,
"preview": "\"\"\"Hacs validation manager.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom importlib import import_module\ni"
},
{
"path": "custom_components/hacs/validate/topics.py",
"chars": 730,
"preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom .base import ActionValidationBase, Validation"
},
{
"path": "custom_components/hacs/websocket/__init__.py",
"chars": 4187,
"preview": "\"\"\"Register_commands.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom homeassistant."
},
{
"path": "custom_components/hacs/websocket/critical.py",
"chars": 1643,
"preview": "\"\"\"Register info websocket commands.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom"
},
{
"path": "custom_components/hacs/websocket/repositories.py",
"chars": 7083,
"preview": "\"\"\"Register info websocket commands.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom typing import TYPE_CHECKING"
},
{
"path": "custom_components/hacs/websocket/repository.py",
"chars": 12091,
"preview": "\"\"\"Register info websocket commands.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom"
},
{
"path": "hacs.json",
"chars": 151,
"preview": "{\n \"name\": \"HACS\",\n \"zip_release\": true,\n \"hide_default_branch\": true,\n \"homeassistant\": \"2025.3.0\",\n \"hacs\": \"0.19"
},
{
"path": "info.md",
"chars": 365,
"preview": "## Useful links\n\n- [General documentation](https://hacs.xyz/)\n- [Configuration](https://hacs.xyz/docs/configuration/basi"
},
{
"path": "pyproject.toml",
"chars": 2043,
"preview": "[tool.isort]\n# https://github.com/PyCQA/isort/wiki/isort-Settings\nprofile = \"black\"\n# will group `import x` and `from x "
},
{
"path": "requirements_action.txt",
"chars": 104,
"preview": "aiogithubapi==26.0.0\nhomeassistant==2025.11.3\n# https://github.com/aio-libs/aiodns/issues/214\npycares<5\n"
},
{
"path": "requirements_base.txt",
"chars": 137,
"preview": "aiogithubapi>=21.11.0\naiohttp>=3.8.3,<4.0\naiohttp_cors==0.7.0\nasync-timeout>=4.0.2\nasynctest==0.13.0\ncolorlog==6.10.1\nse"
},
{
"path": "requirements_core_min.txt",
"chars": 163,
"preview": "# https://github.com/home-assistant/core/blob/2025.3.0/homeassistant/components/frontend/manifest.json\nhome-assistant-fr"
},
{
"path": "requirements_generate_data.txt",
"chars": 134,
"preview": "--requirement requirements_base.txt\nawscli==1.44.58\nhomeassistant==2025.3.0\n# https://github.com/aio-libs/aiodns/issues/"
},
{
"path": "requirements_lint.txt",
"chars": 153,
"preview": "--requirement requirements_base.txt\ncodespell==2.4.2\nisort==8.0.1\npre-commit==4.5.1\npre-commit-hooks==6.0.0\npyupgrade==3"
},
{
"path": "requirements_test.txt",
"chars": 259,
"preview": "--requirement requirements_base.txt\nasynctest==0.13.0\nfreezegun==1.5.5\njosepy<3\n# https://github.com/aio-libs/aiodns/iss"
},
{
"path": "scripts/__init__.py",
"chars": 19,
"preview": "\"\"\"HACS Script.\"\"\"\n"
},
{
"path": "scripts/clear_storage",
"chars": 111,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nrm -rf config/.storage/hacs\nrm -f config/.storage/hacs*\n\n"
},
{
"path": "scripts/coverage",
"chars": 143,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nbash scripts/test > /dev/null\npython3 -m \\\n coverage \\\n repo"
},
{
"path": "scripts/data/__init__.py",
"chars": 24,
"preview": "\"\"\"HACS Data script.\"\"\"\n"
},
{
"path": "scripts/data/common.py",
"chars": 810,
"preview": "\"\"\"Common helpers for data.\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom typing import Any\n\nimport voluptuous "
},
{
"path": "scripts/data/generate_category_data.py",
"chars": 20961,
"preview": "\"\"\"Generate HACS compliant data.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime\nim"
},
{
"path": "scripts/data/validate_category_data.py",
"chars": 2368,
"preview": "\"\"\"Validate HACS V2 data.\"\"\"\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport os\nimport sys\nfrom ty"
},
{
"path": "scripts/develop",
"chars": 941,
"preview": "#!/usr/bin/env bash\n\ndeclare frontend_dir\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nif [ ! -f \"${PWD}/config/configuration.yaml\""
},
{
"path": "scripts/install/core",
"chars": 131,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/../..\"\n\nbash scripts/install/pip_packages --requirement requirements_co"
},
{
"path": "scripts/install/core_dev",
"chars": 180,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/../..\"\n\nbash scripts/install/pip_packages \\\n \"git+https://github.com"
},
{
"path": "scripts/install/frontend",
"chars": 1515,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/../..\"\n\nFRONTEND_VERSION=\"20250128065759\"\n\nfunction installFrontendFrom"
},
{
"path": "scripts/install/pip_packages",
"chars": 157,
"preview": "#!/usr/bin/env bash\n\nset -e\n\npython3 -m pip \\\n install \\\n --upgrade \\\n --disable-pip-version-check \\\n --cons"
},
{
"path": "scripts/install/uv_packages",
"chars": 98,
"preview": "#!/usr/bin/env bash\n\nset -e\n\nuv pip \\\n install \\\n --constraint constraints.txt \\\n \"${@}\"\n"
},
{
"path": "scripts/lgtm.js",
"chars": 47,
"preview": "console.log(\"Dummy file to make LGTM happy...\")"
},
{
"path": "scripts/lint",
"chars": 261,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\npre-commit install-hooks --config .github/pre-commit-config.yaml;\n"
},
{
"path": "scripts/setup",
"chars": 394,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nscripts/install/pip_packages \"pip<23.2,>=21.3.1\"\nscripts/install/p"
},
{
"path": "scripts/snapshot-update",
"chars": 115,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\npython3 -m \\\n pytest \\\n tests \\\n --snapshot-update\n\n"
},
{
"path": "scripts/test",
"chars": 82,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\npython3 -m pytest -x tests\n\n"
},
{
"path": "scripts/update/__init__.py",
"chars": 26,
"preview": "\"\"\"HACS update script.\"\"\"\n"
},
{
"path": "scripts/update/default_repositories.py",
"chars": 1362,
"preview": "\"\"\"Update the shipped default repositories data file.\"\"\"\nimport json\nimport os\nimport sys\n\n\ndef update():\n \"\"\"Update "
},
{
"path": "scripts/update/manifest.py",
"chars": 933,
"preview": "\"\"\"Update the manifest file.\"\"\"\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nMANIFEST_FILE = Path(f\"{os.ge"
},
{
"path": "tests/__init__.py",
"chars": 509,
"preview": "import json\n\nfrom awesomeversion import AwesomeVersion\nfrom homeassistant import const, core, loader\n\n_async_suggest_rep"
},
{
"path": "tests/action/test_hacs_action_integration.py",
"chars": 3565,
"preview": "\"\"\"Tests for the HACS action.\"\"\"\nimport json\nimport os\nfrom unittest import mock\n\nimport pytest\n\nfrom tests.common impor"
},
{
"path": "tests/common.py",
"chars": 19125,
"preview": "# pylint: disable=missing-docstring,invalid-name\nfrom __future__ import annotations\n\nfrom collections.abc import Iterabl"
},
{
"path": "tests/conftest.py",
"chars": 16511,
"preview": "\"\"\"Set up some common test helper things.\"\"\"\nfrom . import patch_time # isort:skip\n\nimport asyncio\nfrom collections.abc"
},
{
"path": "tests/fixtures/proxy/api.github.com/rate_limit.json",
"chars": 1491,
"preview": "{\n \"resources\": {\n \"core\": {\n \"limit\": 5000,\n \"used\": 1,\n \"remaining\": 4999,\n"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/appdaemon.json",
"chars": 860,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"appdaemon\",\n \"path\": \"appdaemon\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/integration.json",
"chars": 982,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"integration\",\n \"path\": \"integration\",\n"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/plugin.json",
"chars": 835,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"plugin\",\n \"path\": \"plugin\",\n \"conte"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/python_script.json",
"chars": 896,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"python_script\",\n \"path\": \"python_scrip"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/template.json",
"chars": 853,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"template\",\n \"path\": \"template\",\n \"c"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/default/contents/theme.json",
"chars": 828,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"theme\",\n \"path\": \"theme\",\n \"content"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json",
"chars": 1065,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"manifest.json\",\n \"path\": \"custom_compo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/integration/contents/hacs.json",
"chars": 987,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/integration/git/trees/main.json",
"chars": 2165,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs/integration.json",
"chars": 7042,
"preview": "{\n \"id\": 172733314,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxNzI3MzMzMTQ=\",\n \"name\": \"integration\",\n \"full_name\": \"h"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/addon-basic/git/trees/main.json",
"chars": 842,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org//trees/9"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/addon-basic/releases.json",
"chars": 2,
"preview": "[]"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/addon-basic.json",
"chars": 29414,
"preview": "{\n \"id\": 1296269,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"addon\",\n \"full_name\": \"hacs-test-org"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/contents/apps/example.json",
"chars": 960,
"preview": "[\n {\n \"type\": \"file\",\n \"size\": 0,\n \"name\": \"__init__.py\",\n \"path\": \"apps/example/__init__"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/contents/apps.json",
"chars": 895,
"preview": "[\n {\n \"type\": \"dir\",\n \"size\": 0,\n \"name\": \"example\",\n \"path\": \"apps/example\",\n \"sh"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/contents/hacs.json",
"chars": 981,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/git/trees/1.0.0.json",
"chars": 1532,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/appdaemo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/git/trees/2.0.0.json",
"chars": 1532,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/appdaemo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/git/trees/main.json",
"chars": 1532,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/appdaemo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic/releases.json",
"chars": 2183,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/appdaemon-basic/releases/1\",\n \"html_url\": \"htt"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/appdaemon-basic.json",
"chars": 30184,
"preview": "{\n \"id\": 1296265,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"appdaemon\",\n \"full_name\": \"hacs-test"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/contents/custom_components/example/manifest.json",
"chars": 1245,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"manifest.json\",\n \"path\": \"custom_compo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/contents/hacs.json",
"chars": 999,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/git/trees/1.0.0.json",
"chars": 1889,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/git/trees/2.0.0.json",
"chars": 2249,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/git/trees/main.json",
"chars": 1889,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/releases/latest.json",
"chars": 3969,
"preview": "{\n \"url\": \"https://api.github.com/repos/hacs-test-org/integration-basic/releases/1\",\n \"html_url\": \"https://github."
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic/releases.json",
"chars": 17078,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/integration-basic/releases/4\",\n \"html_url\": \"h"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/branches/main.json",
"chars": 5487,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/contents/custom_components/example/manifest.json",
"chars": 1258,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"manifest.json\",\n \"path\": \"custom_compo"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/contents/hacs.json",
"chars": 1048,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/git/trees/1.0.0.json",
"chars": 1931,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/git/trees/2.0.0.json",
"chars": 2298,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/git/trees/main.json",
"chars": 1931,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom/releases.json",
"chars": 4325,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/integration-basic-custom/releases/1\",\n \"html_u"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic-custom.json",
"chars": 31727,
"preview": "{\n \"id\": 91296269,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"integration\",\n \"full_name\": \"hacs-t"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-basic.json",
"chars": 30494,
"preview": "{\n \"id\": 1296269,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"integration\",\n \"full_name\": \"hacs-te"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-invalid/git/trees/main.json",
"chars": 1542,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/integrat"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-invalid/releases.json",
"chars": 2,
"preview": "[]"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/integration-invalid.json",
"chars": 30822,
"preview": "{\n \"id\": 1296269,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"addon\",\n \"full_name\": \"hacs-test-org"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/contents/hacs.json",
"chars": 964,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/git/trees/1.0.0.json",
"chars": 1205,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-b"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/git/trees/2.0.0.json",
"chars": 1205,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-b"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/git/trees/main.json",
"chars": 1205,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-b"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic/releases.json",
"chars": 2165,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-basic/releases/1\",\n \"html_url\": \"https:"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-basic.json",
"chars": 29504,
"preview": "{\n \"id\": 1296267,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"plugin\",\n \"full_name\": \"hacs-test-or"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/contents/hacs.json",
"chars": 1006,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/git/trees/1.0.0.json",
"chars": 1544,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-c"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/git/trees/2.0.0.json",
"chars": 1544,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-c"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/git/trees/main.json",
"chars": 1544,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-c"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist/releases.json",
"chars": 2201,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/plugin-custom-dist/releases/1\",\n \"html_url\": \""
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/plugin-custom-dist.json",
"chars": 30561,
"preview": "{\n \"id\": 12962674,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"plugin\",\n \"full_name\": \"hacs-test-o"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/contents/hacs.json",
"chars": 1009,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/git/trees/1.0.0.json",
"chars": 1558,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/python_s"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/git/trees/2.0.0.json",
"chars": 1558,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/python_s"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/git/trees/main.json",
"chars": 1558,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/python_s"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic/releases.json",
"chars": 2207,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/python_script-basic/releases/1\",\n \"html_url\": "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/python_script-basic.json",
"chars": 31104,
"preview": "{\n \"id\": 1296262,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"python_script\",\n \"full_name\": \"hacs-"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/branches/main.json",
"chars": 5403,
"preview": "{\n \"name\": \"main\",\n \"commit\": {\n \"sha\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n \"node_id\": \"MDY"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/contents/hacs.json",
"chars": 1022,
"preview": "{\n \"type\": \"file\",\n \"encoding\": \"base64\",\n \"size\": 5362,\n \"name\": \"hacs.json\",\n \"path\": \"hacs.json\",\n "
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/git/trees/1.0.0.json",
"chars": 1211,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/template"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/git/trees/2.0.0.json",
"chars": 1211,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/template"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/git/trees/main.json",
"chars": 1211,
"preview": "{\n \"sha\": \"9fb037999f264ba9a7fc6274d15fa3ae2ab98312\",\n \"url\": \"https://api.github.com/repos/hacs-test-org/template"
},
{
"path": "tests/fixtures/proxy/api.github.com/repos/hacs-test-org/template-basic/releases.json",
"chars": 4245,
"preview": "[\n {\n \"url\": \"https://api.github.com/repos/hacs-test-org/template-basic/releases/1\",\n \"html_url\": \"http"
}
]
// ... and 501 more files (download for full content)
About this extraction
This page contains the full source code of the hacs/integration GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 701 files (2.3 MB), approximately 642.5k tokens, and a symbol index with 655 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.