98 Commits
0.5.9 ... main

Author SHA1 Message Date
599b8e0b7d Remove stale update entities after uninstall 2026-03-23 17:50:41 +01:00
313fcf94e7 Bump version to 0.7.6 2026-03-23 17:20:29 +01:00
d976ab56e3 Fix blueprint uninstall and restore messaging 2026-03-23 17:14:22 +01:00
80c1c2966f Restore blueprint backup restore support 2026-03-23 17:04:08 +01:00
de3fbf1a12 Fix blueprint update backups 2026-03-23 16:58:38 +01:00
a55281938c Add blueprint support and refresh documentation 2026-03-23 16:42:52 +01:00
48f8ef6265 Prepare blueprint install support 2026-03-23 16:31:16 +01:00
9448176ff4 Bump version to 0.7.4 2026-03-23 15:46:34 +01:00
a029738ec8 Add pinned repositories 2026-03-23 15:22:53 +01:00
754540d578 CHANGELOG.md aktualisiert 2026-03-23 13:48:54 +00:00
1103c5e326 Bump version to 0.7.3 2026-03-23 14:47:13 +01:00
b7d4d0ded4 Add release notes and project prompts 2026-03-23 14:09:29 +01:00
a8ff892993 add 0.7.2 2026-01-20 08:15:43 +00:00
90223e3fc4 0.7.2 2026-01-20 08:15:14 +00:00
0f5504b67d 0.7.2 2026-01-20 08:14:58 +00:00
5fff1b2692 add 0.7.1 2026-01-20 07:19:03 +00:00
c8356c7603 0.7.1 2026-01-20 07:17:45 +00:00
0c49a50fc9 0.7.1 2026-01-20 07:17:24 +00:00
fa48841645 0.7.1 2026-01-20 07:16:48 +00:00
1445fff739 add 0.7.0 2026-01-20 05:50:18 +00:00
5cf365f354 0.7 0 2026-01-20 05:48:27 +00:00
f73ce4095c 0.7.0 2026-01-20 05:47:49 +00:00
1484d53f8c 0.7.0 2026-01-20 05:47:00 +00:00
0e99c9c59e 0.7.0 2026-01-20 05:46:20 +00:00
644e61aab0 0.7.0 2026-01-20 05:45:41 +00:00
4c2a104af7 0.7.0 2026-01-20 05:45:02 +00:00
95dd8b9dc2 0.7.0 2026-01-20 05:44:32 +00:00
8b01c04a4c 0 7.0 2026-01-20 05:43:57 +00:00
981f56a693 add 0.6.9 2026-01-19 17:51:36 +00:00
f6bd04f354 0.6.9 2026-01-19 17:42:35 +00:00
8da8a26a90 0.6.9 2026-01-19 17:41:41 +00:00
42fe5afe52 0.6.9 2026-01-19 17:41:18 +00:00
437c020566 0.6.9 2026-01-19 17:40:27 +00:00
b863ed4d51 0.6.9 2026-01-19 17:39:37 +00:00
368642345d Add on 0.6.9 2026-01-19 17:39:09 +00:00
43bc31c8b4 0.6.9 2026-01-19 16:52:59 +00:00
357049aa7b add 0.6.8 2026-01-19 14:35:56 +00:00
d85ef5621c 0.6.8 2026-01-19 14:35:24 +00:00
faac180212 0.6.8 2026-01-19 14:35:05 +00:00
76d8a45f37 README_FULL.md aktualisiert 2026-01-19 09:00:21 +00:00
d68d322df6 README_DEVELOPER.md aktualisiert 2026-01-19 08:27:01 +00:00
113c951028 Full 2026-01-19 08:09:48 +00:00
2a4ab676ec Ne 2026-01-19 08:06:42 +00:00
c0a04f505e README.md aktualisiert 2026-01-19 07:53:33 +00:00
a63006bb07 Add full 2026-01-19 07:42:35 +00:00
f8e678215d Add Developer 2026-01-19 07:41:31 +00:00
f745f8ec1e Add readme 2026-01-19 07:39:35 +00:00
33502a6d99 add 0.6.7 2026-01-19 07:16:35 +00:00
1306ee9518 Change 0.6.7 2026-01-19 07:15:48 +00:00
e37546cab1 0.6 7 2026-01-19 07:15:30 +00:00
88c3233fd1 0.6.7 2026-01-19 07:14:58 +00:00
02f3047080 0.6 7 2026-01-19 07:14:22 +00:00
d4012589e6 add 0.6.6 2026-01-18 19:55:59 +00:00
8ac67fa60c 0.6.6 2026-01-18 19:53:34 +00:00
981490c152 0.6.6 2026-01-18 19:53:12 +00:00
99b2a0f0c5 0.6.6 2026-01-18 19:52:27 +00:00
7ead494765 0.6.6 2026-01-18 19:51:31 +00:00
342b6f6c57 0.6.6 2026-01-18 19:50:44 +00:00
66ca63b2be I 2026-01-18 19:35:19 +00:00
e8325f722f I 2026-01-18 19:32:58 +00:00
7c1a91937a add 0.6.5 2026-01-18 18:55:40 +00:00
7ac3289bb7 0.6.5 2026-01-18 18:53:52 +00:00
19bdbd1b9a 0.6.5 2026-01-18 18:53:30 +00:00
24363cd2ac 0.6.5 2026-01-18 18:52:55 +00:00
e19ca5bff1 0.6.5 2026-01-18 18:51:48 +00:00
05897d4370 0.6.5 2026-01-18 18:51:17 +00:00
7a3a28d87f add 0.6.4 2026-01-18 16:58:08 +00:00
240cded8a9 0.6.4 2026-01-18 16:56:23 +00:00
31e241f052 0.6.4 2026-01-18 16:56:04 +00:00
de579682a0 0.6.4 2026-01-18 16:55:36 +00:00
9acbd5046c Add 0.6.3 2026-01-18 15:54:21 +00:00
8d63c88e69 0.6.3 2026-01-18 15:53:52 +00:00
cffb0af60e 0.6.3 2026-01-18 15:53:03 +00:00
857b7a127a 0.6 3 2026-01-18 15:52:28 +00:00
66b24ece48 0.6.3 2026-01-18 15:52:01 +00:00
0cc3b466e0 0.6.3 2026-01-18 15:51:34 +00:00
f1e03b31a1 add 0.6.2 2026-01-18 13:16:42 +00:00
4e12d596d6 0.6.2 2026-01-18 13:12:47 +00:00
fa97f89afb 0.6.2 2026-01-18 13:12:21 +00:00
0718bee185 0.6.2 2026-01-18 13:11:39 +00:00
1a53107450 0.6.2 2026-01-18 13:10:59 +00:00
ab82cc6fd3 0.6.2 2026-01-18 13:10:16 +00:00
8e51f144e1 0.6.2 2026-01-18 13:09:37 +00:00
f292e22301 Fix restore version 2026-01-18 09:08:25 +00:00
2eb194c001 Add 0.6 1 2026-01-18 09:07:40 +00:00
f4e367987a 0.6.1 2026-01-18 09:07:03 +00:00
08aa4b5e15 0.6.0 2026-01-18 08:37:07 +00:00
b1676482f0 0.6.0 2026-01-18 08:36:34 +00:00
e46cd6e488 0.6.0 2026-01-18 08:34:44 +00:00
edd2fdd3fb 0.6.0 2026-01-18 08:33:34 +00:00
a4a0c1462b 0.6.0 2026-01-18 08:32:51 +00:00
196e63c08e 0.6.0 2026-01-18 08:32:06 +00:00
518ac1d59d add 0.5.11 2026-01-18 07:47:24 +00:00
ad699dc69a 0.5.11 2026-01-18 07:45:56 +00:00
a8e247d288 Add backup 2026-01-18 07:45:34 +00:00
318d517575 0.5.10 2026-01-17 20:49:10 +00:00
db137be5b1 0.5.1p 2026-01-17 20:48:07 +00:00
83cec0f75a 0.5.10 2026-01-17 20:47:39 +00:00
22 changed files with 4018 additions and 166 deletions

3
.gitignore vendored
View File

@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# Local project instructions
.idea/start prompt.md

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/Projects.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

57
.idea/changes.md generated Normal file
View File

@@ -0,0 +1,57 @@
# Changes Log
## 2026-03-23
### Added
- Created `.idea/start prompt.md` as a persistent project working prompt for future sessions.
- Added `.idea/changes.md` as the preferred in-project location for the detailed session change log.
- Added release notes support in the active Bahmcloud Store runtime path: backend provider fetching, a dedicated API endpoint, and panel display for the currently selected version when provider release notes are available.
- Bumped the Home Assistant panel asset cache-buster from `v=109` to `v=110` so the updated frontend loads reliably after deployment.
- Bumped the integration version from `0.7.2` to `0.7.3`.
- Added the `0.7.3` release entry to `CHANGELOG.md` with the new release-notes and project-workflow changes.
- Added a persistent rule that commit messages, pushes, release notes, and changelog entries must never mention prompts, AI/KI tools, or `.idea` folder files.
- Added a persistent release rule that future git tags and release names must use plain version numbers without a leading `v`.
- Added a broader persistent rule that internal workflow or prompt/process decisions must never appear in any public-facing project communication.
- Added a persistent language rule that all project-facing repository content must stay in English regardless of the chat language.
- Added persistent pinned repositories support: favorites are stored in settings, exposed by the backend, filterable and sortable in the store view, and toggleable from the detail view without forcing a full repository refresh.
- Bumped the integration version from `0.7.3` to `0.7.4` and added the `0.7.4` release entry to `CHANGELOG.md` for the pinned-repositories feature.
- Added the broader product roadmap to the persistent project prompt: blueprints, templates, Lovelace designs, and more category support as future store targets.
- Started the blueprint/category-aware installer preparation: installation metadata now distinguishes install type and installed paths, repo payloads expose install targets, and the active panel shows install-target context for future non-integration categories.
- Added initial blueprint install-path handling groundwork so the codebase is no longer fully hard-wired to `custom_components/`.
- Bumped the integration version from `0.7.4` to `0.7.5` and added the `0.7.5` release entry to `CHANGELOG.md` for blueprint support and the documentation refresh.
- Fixed blueprint update backups: blueprint file updates now create content backups before overwrite and can roll back copied blueprint files if installation fails.
- Kept the no-restart behavior for blueprints, because blueprint deployment does not normally require a Home Assistant restart.
- Restored blueprint backup/restore availability in the UI and backend: the restore button is visible again for blueprint installs, blueprint backups can be listed, and blueprint content can now be restored from backup without forcing a restart.
- Fixed category-specific uninstall and restore messaging in the active panel: blueprints now reference `/config/blueprints` and explicitly state that no restart is required, while integrations keep the `/config/custom_components` restart warning.
- Updated the public documentation set (`README.md`, `README_DEVELOPER.md`, `README_FULL.md`, and `CHANGELOG.md`) for the completed blueprint backup, restore, uninstall, and restart-behavior work, and bumped the integration version to `0.7.6` for the next release.
- Fixed stale Home Assistant update entities after uninstall: BCS now removes orphaned update entities from the running platform state and entity registry when an installed repository or blueprint is uninstalled.
### Documented
- Captured the verified project identity from the repository and README files: Bahmcloud Store is a Home Assistant custom integration intended to behave like a provider-neutral store for custom integrations, similar to HACS but broader than GitHub-only workflows.
- Recorded the actual active architecture from source analysis, including the config-entry-only setup, the fixed Bahmcloud store index, delayed startup refresh, periodic refresh, repo merge flow, cache usage, install/update/uninstall pipeline, backup/restore pipeline, update entities, and Repairs-based restart handling.
- Recorded the current provider reality from code: GitHub, GitLab, and Gitea-compatible repositories are the concrete supported paths today, while truly generic "all git providers" support is still an intention and must be validated case by case.
- Recorded the public API endpoints exposed by `views.py` so future work preserves the current backend contract unless a deliberate breaking change is approved.
- Recorded storage facts from `storage.py`, including the `bcs_store` Home Assistant storage key and the persisted sections for custom repositories, installed repositories, settings, HACS cache, and repo enrichment cache.
- Recorded frontend facts from the active panel registration in `__init__.py` and the active frontend implementation in `panel/panel.js`, including the cache-busting panel asset version query.
- Updated the persistent start prompt to point future work to `.idea/changes.md` as the canonical detailed work log.
- Release notes are intentionally tied to provider release objects, so tags or branches without release bodies now return a clear "not available" state instead of misleading fallback text.
### Important findings from code analysis
- Identified `custom_components/bahmcloud_store/panel/panel.js` as the active Home Assistant panel script currently loaded by the integration.
- Identified `custom_components/bahmcloud_store/panel/app.js`, `custom_components/bahmcloud_store/panel/index.html`, and `custom_components/bahmcloud_store/panel/styles.css` as likely legacy or secondary assets that should not be treated as authoritative without verification.
- Identified `custom_components/bahmcloud_store/store.py` as an older implementation with a different data model and API shape than the active `BCSCore` runtime.
- Identified `custom_components/bahmcloud_store/custom_repo_view.py` as duplicate or legacy API code because the active custom-repo handling already exists in `views.py`.
- Noted that the README set is directionally useful but not fully authoritative where it conflicts with current code behavior.
- Noted that some repository files contain encoding or mojibake artifacts, so future edits should preserve valid UTF-8 and avoid spreading broken text.
### Project rules written into the start prompt
- Never push, commit, tag, or create a release without explicit user approval.
- Always append a dated and detailed entry to `.idea/changes.md` for every change made.
- When a release is created, collect all relevant changes since the last release into `CHANGELOG.md`.
### Verification
- Reviewed repository structure and current git status.
- Read `README.md`, `README_DEVELOPER.md`, `README_FULL.md`, `bcs.yaml`, and the current `CHANGELOG.md`.
- Analyzed the active backend files: `__init__.py`, `const.py`, `core.py`, `providers.py`, `metadata.py`, `storage.py`, `views.py`, `config_flow.py`, `update.py`, and `repairs.py`.
- Checked panel and legacy-related files to distinguish the currently active UI path from older or duplicated files.
- Verified that the active panel (`panel/panel.js`) now requests release notes from the new backend route and reloads them when the selected install version changes.

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Projects.iml" filepath="$PROJECT_DIR$/.idea/Projects.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -11,6 +11,226 @@ Sections:
---
## 0.7.6 - 2026-03-23
### Added
- Blueprint backup listing and restore support in the repository detail view.
### Changed
- Install, update, uninstall, and restore feedback now reflects whether a restart is actually required for the selected content type.
- Blueprint restore dialogs and action messages now reference `/config/blueprints` instead of integration-only paths.
### Fixed
- Blueprint updates now create usable content backups before overwriting files.
- Blueprint backups can be restored again from the active store UI after the previous restore-button regression.
- Blueprint uninstall confirmation text now matches the real uninstall behavior and no longer incorrectly warns about `/config/custom_components` or mandatory restarts.
## 0.7.5 - 2026-03-23
### Added
- Initial blueprint install support with deployment to the Home Assistant blueprints folder.
- Category-aware install metadata including install type, install target, and installed paths.
- Install target details in the repository view for supported content categories.
### Changed
- Store documentation refreshed to match the current feature set, including pinned repositories, release notes, and blueprint support.
- Non-integration content handling is now prepared for category-specific install strategies instead of assuming `custom_components` only.
- Frontend asset version updated so the latest store UI changes load immediately after deployment.
## 0.7.4 - 2026-03-23
### Added
- Pinned repositories support to let admins keep important integrations easy to find.
- Pin and unpin actions in the repository detail view.
- Store filtering and sorting options for pinned repositories.
### Changed
- Repository favorites are now persisted in settings and restored across reloads.
- Pinned repositories are highlighted in the store overview with a dedicated badge.
- Frontend asset version updated so the latest panel changes load immediately after deployment.
## 0.7.3 - 2026-03-23
### Added
- Release notes support in the repository detail view for provider releases.
- New backend endpoint to fetch release notes for the selected repository version.
### Changed
- Home Assistant panel asset cache-buster bumped to ensure updated frontend assets load after deployment.
- Release-note lookups now clearly report when a selected version has no provider release body instead of implying notes exist for plain tags or branches.
## 0.7.2 2026-01-20
### Fixed
- When Bahmcloud Store is installed via an external installer (files copied into /config/custom_components), it now reconciles itself as "installed" in BCS storage so update checks work immediately.
## 0.7.1 2026-01-20
### Fixed
- GitHub version provider now reliably fetches the latest 20 releases/tags using authenticated API requests.
- Repositories that were previously fetched in a degraded state (only `latest` and `branch`) are now automatically refreshed on repository view.
- Cached version lists with incomplete data are no longer reused and are re-fetched from the provider.
## [0.7.0] - 2026-01-20
### Added
- Options dialog (gear icon) for the Bahmcloud Store integration.
- Optional GitHub token can now be set, changed or removed via the Home Assistant UI.
### Fixed
- Fixed missing options flow when clicking the integration settings button.
## [0.6.9] 2026-01-19
### Added
- New Home Assistant **GUI setup** (Config Flow) no YAML configuration required.
- Optional **GitHub Token** support to increase API limits (up to 5000 req/h).
Configurable via *Integration → Options*.
- Clear setup guidance and warning about GitHub rate limits.
- Automatic detection and warning if YAML setup is still present (ignored safely).
### Changed
- **store.yaml** URL is now fixed to the official Bahmcloud Store index:
https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml
- Installation workflow fully aligned with standard HA integrations.
- Update platform migrated to `async_setup_entry`.
### Fixed
- Minor stability and persistence improvements in startup sequence.
- Prevented duplicate background initialization when HA reloads the integration.
### Notes
- To enable extended GitHub access, create a fine-grained personal access token
(read-only) at https://github.com/settings/tokens and add it via the integration options.
## 0.6.8 Performance & Cache Stabilization (2026-01-19)
### Fixed
- Fixed excessive GitHub API requests causing rate limiting
- Fixed missing persistence of repository versions and metadata
- Fixed background enrichment re-running after Home Assistant restart
### Changed
- Repository metadata (versions, README, descriptions) is now fetched once in the background
- Cached data is reused and only refreshed on explicit user interaction
- Improved separation between startup refresh and on-demand updates
### Improved
- Significantly reduced Home Assistant startup time
- Greatly reduced GitHub API usage
- More reliable version selection for GitHub and HACS repositories
### Notes
- Background tasks may still appear in DEBUG logs (expected behavior)
- No functional UI changes in this release
## [0.6.7] - 2026-01-19
### Fixed
- Repository metadata loaded on demand is now persisted and restored after Home Assistant restart.
- Background enrichment reliably updates latest versions and descriptions for HACS repositories.
- Cached repository data is correctly restored on startup for non-installed repositories.
### Changed
- Repository details are only considered fully enriched once latest version information is available.
- Improved reliability of background cache synchronization without impacting startup performance.
## [0.6.6] - 2026-01-18
### Added
- Source filter to limit repositories by origin: BCS Official, HACS, or Custom.
- Visual source badges for repositories (BCS Official, HACS, Custom).
- Restored HACS enable/disable toggle in the Store UI.
### Changed
- HACS repositories now display human-readable names and descriptions based on official HACS metadata.
- Improved Store usability on mobile devices by fixing back navigation from repository detail view.
### Fixed
- Fixed missing HACS toggle after UI updates.
- Fixed mobile browser back button exiting the Store instead of returning to the repository list.
## [0.6.5] - 2026-01-18
### Added
- Separate handling of HACS official repositories with an enable/disable toggle in the Store UI.
- HACS repositories are now loaded independently from the main store index.
### Changed
- Store index can remain minimal and curated; HACS repositories are no longer required in store.yaml.
- Improved Store performance and clarity by clearly separating repository sources.
### Fixed
- Browser cache issues resolved by proper panel cache-busting for UI updates.
### Internal
- No changes to install, update, backup, or restore logic.
- Fully backward compatible with existing installations and configurations.
## [0.6.4] - 2026-01-18
### Fixed
- Fixed long Home Assistant startup times caused by background repository enrichment running too early.
### Changed
- Background repository enrichment is now started only after Home Assistant has fully started.
- Repository cache updates now run fully asynchronous without blocking Home Assistant startup.
### Internal
- Improved alignment with Home Assistant startup lifecycle.
- No functional changes to store behavior or UI.
## [0.6.3] - 2026-01-18
### Changed
- Improved Store performance for large indexes by avoiding full metadata enrichment during list refresh.
- Repository details are loaded on demand, reducing initial load time and network requests.
- Index refresh is skipped when the index content has not changed.
## [0.6.2] - 2026-01-18
### Added
- Selectable install/update version per repository (install older releases/tags to downgrade when needed).
- New API endpoint to list available versions for a repository: `GET /api/bcs/versions?repo_id=...`.
## [0.6.1] - 2026-01-18
### Fixed
- Restored integrations now correctly report the restored version instead of the latest installed version.
- Update availability is correctly recalculated after restoring a backup, allowing updates to be applied again.
- Improved restore compatibility with backups created before version metadata was introduced.
## [0.6.0] - 2026-01-18
### Added
- Restore feature with selection of the last available backups (up to 5 per domain).
- New API endpoints to list and restore backups:
- `GET /api/bcs/backups?repo_id=...`
- `POST /api/bcs/restore?repo_id=...&backup_id=...`
### Safety
- Restoring a backup triggers a “restart required” prompt to apply the recovered integration state.
### Notes
- This is a major release milestone consolidating install/update/uninstall, backup/rollback, and restore workflows.
## [0.5.11] - 2026-01-18
### Added
- Automatic backup of existing custom components before install or update.
- Backup retention with a configurable limit per domain.
### Safety
- Automatic rollback is triggered if an install or update fails after a backup was created.
## [0.5.10] - 2026-01-17
### Added
- Store view controls: Filter and Sort dropdowns alongside the existing Category selector.
### Fixed
- Search input focus is preserved while typing (cursor no longer jumps out after re-render).
## [0.5.9] - 2026-01-17
### Changed

View File

@@ -1,3 +1,86 @@
# bahmcloud_store
# Bahmcloud Store (BCS) for Home Assistant
Bahmcloud Store for installing costum_components to Homeassistant
Bahmcloud Store (BCS) is a provider-neutral store for Home Assistant content hosted on git platforms.
It currently supports installing and managing:
- Integrations
- Blueprints
Supported providers today:
- GitHub
- GitLab
- Gitea / Forgejo-style hosts
- Custom repositories added by URL when the host behaves compatibly
This README is for end users.
---
## Features
- Browse repositories from the official BCS index, HACS integration source, and custom repository URLs
- Install, update, downgrade, and uninstall integrations directly from Home Assistant
- Install blueprint repositories directly into the Home Assistant blueprints folder
- Read repository README files inside the store UI
- View release notes for provider releases
- Pin repositories so important items stay easy to find
- Use native Home Assistant update entities for installed integrations
- Create and restore backups for integration installs and updates
- Create and restore blueprint content backups during blueprint updates and restore actions
---
## Installation
### Option A: Home Assistant OS / Supervised
1. Open **Settings -> Add-ons -> Add-on Store**
2. Add the Bahmcloud add-on repository:
`https://git.bahmcloud.de/bahmcloud/addons`
3. Install **Bahmcloud Store Installer**
4. Start the add-on
5. Restart Home Assistant
Installed to:
`/config/custom_components/bahmcloud_store`
### Option B: Manual Installation
1. Download the latest release
2. Copy `custom_components/bahmcloud_store` to:
`/config/custom_components/bahmcloud_store`
3. Restart Home Assistant
---
## Repository Sources
- **BCS Official**: repositories from the Bahmcloud store index
- **HACS**: optional official HACS integration source
- **Custom**: repositories you add manually by URL
---
## Current Content Support
### Integrations
- Expected repository content: `custom_components/<domain>/...`
- Install target: `/config/custom_components/<domain>`
- Supports install, update, uninstall, backup, restore, version selection, and update entities
- Restart required after install, update, uninstall, or restore
### Blueprints
- Expected repository content: `blueprints/...`
- Install target: `/config/blueprints/...`
- Supports install, update, uninstall, backup, and restore through the store
- No restart is normally required for blueprint deployment
---
## Documentation
- Developer documentation: `README_DEVELOPER.md`
- Full user guide: `README_FULL.md`

159
README_DEVELOPER.md Normal file
View File

@@ -0,0 +1,159 @@
# Bahmcloud Store - Developer Documentation
For contributors and maintainers.
## Project Scope
Bahmcloud Store is evolving from an integration-only store into a broader Home Assistant git-based content store.
Currently supported install categories:
- Integrations
- Blueprints
Planned categories:
- Templates
- Lovelace / dashboard designs
- Additional content types with category-specific install logic
## Repository Layout
Repositories related to the project:
1. Installer add-on:
`https://git.bahmcloud.de/bahmcloud/addons`
2. Core integration:
`https://git.bahmcloud.de/bahmcloud/bahmcloud_store`
3. Store index:
`https://git.bahmcloud.de/bahmcloud/ha_store`
## Integration Layout
`custom_components/bahmcloud_store/`
- `__init__.py`: setup, panel registration, delayed startup refresh, periodic refresh
- `core.py`: index merge, repo enrichment, install/update/uninstall, backup/restore, category-aware install groundwork
- `providers.py`: provider detection, latest version lookup, release notes, README fetching
- `metadata.py`: reads `bcs.yaml`, `hacs.json`, and `hacs.yaml`
- `storage.py`: persistent storage for installed repos, settings, caches
- `views.py`: HTTP API
- `update.py`: Home Assistant update entities
- `repairs.py`: restart-required repair flow
- `panel/`: active frontend panel
- `manifest.json`
## Runtime Model
- `RepoItem`: merged repository model used by the UI and backend
- Installed repositories: persisted in Home Assistant storage
- Settings: persistent UI and behavior settings such as HACS enablement and pinned repositories
- Repo cache: provider and metadata enrichment cache
- HACS cache: display metadata cache for HACS integration repositories
## Metadata
Metadata priority:
1. `bcs.yaml`
2. `hacs.yaml`
3. `hacs.json`
Common fields:
```yaml
name: Example Project
description: Short description
category: Integrations
author: Example Author
maintainer: Example Maintainer
```
Category currently matters operationally:
- `Integrations` -> install from `custom_components/...`
- `Blueprint` / `Blueprints` -> install from `blueprints/...`
## Supported Install Categories
### Integrations
- Expected source layout: `custom_components/<domain>/manifest.json`
- Install target: `/config/custom_components/<domain>`
- Supports backup, restore, update entities, and restart-required flow
### Blueprints
- Expected source layout: `blueprints/...`
- Install target: `/config/blueprints/...`
- Supports install, update, uninstall, content backup, and content restore
- Restores and uninstalls are path-based and operate on the recorded installed blueprint files
- Category-aware groundwork is in place so future content types can use their own install strategies
## HTTP API
Base path: `/api/bcs`
- `GET /api/bcs`
- `POST /api/bcs?action=refresh`
- `GET /api/bcs/settings`
- `POST /api/bcs/settings`
- `GET /api/bcs/readme?repo_id=...`
- `GET /api/bcs/release_notes?repo_id=...&ref=...`
- `GET /api/bcs/versions?repo_id=...`
- `GET /api/bcs/repo?repo_id=...`
- `POST /api/bcs/install?repo_id=...&version=...`
- `POST /api/bcs/update?repo_id=...&version=...`
- `POST /api/bcs/uninstall?repo_id=...`
- `GET /api/bcs/backups?repo_id=...`
- `POST /api/bcs/restore?repo_id=...&backup_id=...`
- `POST /api/bcs/restart`
- `DELETE /api/bcs/custom_repo?id=...`
## Current UI Features
- Search, source filter, category filter, state filter, sorting
- HACS integration source toggle
- Pinned repositories
- README rendering
- Release notes rendering
- Version selection
- Backup restore UI for integrations and blueprints
## Contributing to the Official Store Index
Add a repository to the BCS store index in `ha_store`.
Example index entry:
```yaml
- name: Example Project
url: https://your-git-hoster.example/org/repo
category: Blueprint
```
Recommended repository metadata:
```yaml
name: Example Project
description: One-line description
category: Blueprint
author: Example Author
maintainer: Example Maintainer
```
Validation should match the category:
- Integrations: verify `custom_components/<domain>/manifest.json`
- Blueprints: verify `blueprints/...`
## Design Direction
The long-term architecture should remain category-aware:
- category -> validation strategy
- category -> install target
- category -> backup / restore behavior
- category -> UI affordances
This is especially important before Templates and Lovelace support are added, because those should stay compatible with established HACS expectations where possible.

324
README_FULL.md Normal file
View File

@@ -0,0 +1,324 @@
# Bahmcloud Store - Full User Guide
This guide explains the current feature set of Bahmcloud Store (BCS) for Home Assistant.
BCS is a git-based store UI that can currently manage integrations and blueprints from multiple repository providers.
---
## Contents
- Concepts
- Sources
- Supported Content Types
- UI Overview
- Finding Repositories
- Installing
- Selecting Versions
- Updating
- Uninstalling
- Pinned Repositories
- Release Notes
- Backups and Restore
- Custom Repositories
- HACS Repositories
- Update Entities
- Performance and Caching
- Restart Behavior
- Troubleshooting
- FAQ
---
## Concepts
### Sources
- **BCS Official**: repositories from the official Bahmcloud index
- **HACS**: optional HACS integration source
- **Custom**: repositories you add manually by URL
### Installed Ref
BCS stores the installed ref separately from any version declared inside the repository itself.
That makes update checks more reliable when a repository uses tags or releases that do not exactly match its internal manifest version.
---
## Supported Content Types
### Integrations
- Source layout: `custom_components/<domain>/...`
- Install target: `/config/custom_components/<domain>`
- Supports install, update, downgrade, uninstall, backups, restore, and update entities
- Restart required after install, update, uninstall, or restore
### Blueprints
- Source layout: `blueprints/...`
- Install target: `/config/blueprints/...`
- Supports install, update, uninstall, backup, and restore through the store
- Intended for blueprint repositories without integration-specific folder structures
---
## UI Overview
Store view:
- Search
- Source filter
- Category filter
- State filter
- Sorting
- HACS source toggle
- Pinned repository filtering and sorting
Repository detail view:
- README rendering
- Release notes for provider releases
- Version selector
- Install / Update / Uninstall actions
- Pin / Unpin action
- Backup restore action for integrations
- Category and install target information
---
## Finding Repositories
1. Use **Search** to filter by name, description, or URL.
2. Use **Source** to switch between BCS Official, HACS, and Custom repositories.
3. Use **Category** to focus on a specific content type.
4. Use **Pinned** filtering or sorting to surface repositories you watch often.
Descriptions, metadata, and latest versions may appear progressively because BCS enriches repository information in the background.
---
## Installing
### Integration Install
1. Open a repository.
2. Optionally select a version.
3. Click **Install**.
4. Restart Home Assistant when prompted.
Internally, BCS:
- downloads the selected repository archive
- extracts `custom_components/...`
- installs every valid integration found there
- stores the installed ref
- creates backups before overwriting existing integration folders
### Blueprint Install
1. Open a blueprint repository.
2. Optionally select a version.
3. Click **Install**.
Internally, BCS:
- downloads the selected repository archive
- extracts `blueprints/...`
- copies blueprint files into `/config/blueprints/...`
Blueprint installs normally do not require a Home Assistant restart.
---
## Selecting Versions
- Use the version selector in the detail view.
- `Latest` uses the newest provider release or tag BCS can determine.
- You can install an older release or tag when available.
- Release notes are shown when the selected version is a provider release with a body.
---
## Updating
### Integrations
- The **Update** button appears when the latest ref differs from the installed ref.
- Updates are also available through **Settings -> Updates** via native Home Assistant update entities.
- Integration updates create backups before overwriting files.
### Blueprints
- Blueprint repositories can also be updated or reinstalled from another selected version.
- Blueprint updates create content backups before overwriting files.
- The current blueprint path handling is focused on direct deployment to the blueprints folder.
---
## Uninstalling
### Integrations
- Removes the installed integration directories from `/config/custom_components`
- Clears the installed state in BCS
- Usually requires a restart
### Blueprints
- Removes the installed blueprint files recorded by BCS
- Cleans up empty directories below `/config/blueprints` when possible
- Does not require a restart under normal conditions
---
## Pinned Repositories
You can pin repositories to keep important items easy to find.
Current behavior:
- Pin or unpin from the detail view
- Filter the store by pinned repositories
- Sort the store with pinned repositories first
- Pinned repositories show a dedicated badge in the store overview
- Pinned state is persisted in settings
---
## Release Notes
When a provider release has a release body, BCS can show release notes in the detail view for the selected version.
Notes:
- Release notes are available for real releases, not every tag or branch
- If no provider release body exists, BCS shows a clear not-available state
---
## Backups and Restore
Integration installs and updates create backups before overwriting existing files.
Blueprint updates also create content backups before overwriting deployed blueprint files.
Backup path:
`/config/.bcs_backups/<domain>/<timestamp>/`
Restore flow:
1. Open the repository detail
2. Click **Restore**
3. Select a backup
4. Confirm restore
5. Restart Home Assistant if prompted
Restore is available for integrations and blueprints.
Blueprint restore flow:
1. Open the blueprint repository detail
2. Click **Restore**
3. Select a backup
4. Confirm restore
Blueprint restores overwrite the recorded installed files under `/config/blueprints/...` and normally do not require a restart.
---
## Custom Repositories
You can add any public repository URL that matches a supported provider style and contains supported content.
BCS attempts to detect:
- provider
- default branch
- latest version
- metadata from `bcs.yaml`, `hacs.yaml`, or `hacs.json`
- README content
Custom repositories get a **Custom** badge and can be filtered separately.
---
## HACS Repositories
Enable the HACS toggle to include official HACS integration repositories.
Current scope:
- HACS source support is currently focused on integrations
- HACS metadata is used to improve naming and descriptions
---
## Update Entities
BCS exposes Home Assistant update entities for installed integrations.
- Found under **Settings -> Updates**
- Shows installed ref and latest ref
- Lets Home Assistant trigger the BCS update pipeline
Blueprint repositories do not currently use the integration-style update entity flow.
---
## Performance and Caching
BCS is designed to keep startup responsive.
- Heavy work runs after Home Assistant startup
- Repo data is enriched in the background
- Repository details are cached
- Version lists are cached
- HACS metadata is cached
Opening a repository detail view can force immediate enrichment for that repository.
---
## Restart Behavior
Restart usually matters only for integration changes.
- Integration install/update/uninstall/restore: restart expected
- Blueprint install/update/uninstall/restore: restart usually not needed
BCS uses a Home Assistant repair flow to surface restart requirements for integration changes.
---
## Troubleshooting
- **Latest version missing**: open the repository detail once to force a recheck
- **Release notes missing**: the selected version may be a tag or branch without a provider release body
- **Blueprint install fails**: verify the repository really contains a `blueprints/` folder
- **Integration install fails**: verify the repository contains `custom_components/<domain>/manifest.json`
- **Repo appears but cannot install**: metadata can describe the category, but the repository layout still has to match that category
---
## FAQ
### Where are integrations installed?
`/config/custom_components/<domain>`
### Where are blueprints installed?
`/config/blueprints/...`
### Are pinned repositories persistent?
Yes.
### Do blueprints require a restart?
Usually no.
### Do integrations require a restart?
Yes, normally after install, update, uninstall, or restore.

View File

@@ -4,7 +4,7 @@ description: >
Supports GitHub, GitLab, Gitea and Bahmcloud repositories with
a central index, UI panel and API, similar to HACS but independent.
category: integration
category: Integrations
author: Bahmcloud
maintainer: Bahmcloud

View File

@@ -3,52 +3,86 @@ from __future__ import annotations
import logging
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.components.panel_custom import async_register_panel
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.helpers.event import async_call_later, async_track_time_interval
from .core import BCSCore, BCSConfig, BCSError
from .const import CONF_GITHUB_TOKEN, DEFAULT_STORE_URL, DOMAIN
from .core import BCSError, BCSConfig, BCSCore
_LOGGER = logging.getLogger(__name__)
DOMAIN = "bahmcloud_store"
DEFAULT_STORE_URL = "https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml"
CONF_STORE_URL = "store_url"
PLATFORMS: list[str] = ["update"]
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
cfg = config.get(DOMAIN, {}) or {}
store_url = cfg.get(CONF_STORE_URL, DEFAULT_STORE_URL)
"""Set up Bahmcloud Store.
core = BCSCore(hass, BCSConfig(store_url=store_url))
hass.data[DOMAIN] = core
We intentionally do NOT support YAML configuration.
This method is kept so we can log a helpful message if someone tries.
"""
if DOMAIN in (config or {}):
_LOGGER.warning(
"BCS YAML configuration is no longer supported. "
"Please remove 'bahmcloud_store:' from configuration.yaml and set up the integration via the UI."
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bahmcloud Store from a config entry (UI setup)."""
# Only one instance.
hass.data.setdefault(DOMAIN, {})
github_token = (entry.options.get(CONF_GITHUB_TOKEN) or "").strip() or None
core = BCSCore(
hass,
BCSConfig(
store_url=DEFAULT_STORE_URL,
github_token=github_token,
),
)
hass.data[DOMAIN][entry.entry_id] = core
# Keep a convenient shortcut for platforms that previously used hass.data[DOMAIN] directly.
hass.data[DOMAIN]["_core"] = core
await core.async_initialize()
# Provide native Update entities in Settings -> System -> Updates.
# This integration is YAML-based (async_setup), therefore we load the platform manually.
await async_load_platform(hass, "update", DOMAIN, {}, config)
# HTTP views + panel (registered once per entry; we only allow one entry).
from .views import (
StaticAssetsView,
BCSApiView,
BCSSettingsView,
BCSReadmeView,
BCSVersionsView,
BCSReleaseNotesView,
BCSRepoDetailView,
BCSCustomRepoView,
BCSInstallView,
BCSUpdateView,
BCSUninstallView,
BCSBackupsView,
BCSRestoreView,
BCSRestartView,
)
hass.http.register_view(StaticAssetsView())
hass.http.register_view(BCSApiView(core))
hass.http.register_view(BCSSettingsView(core))
hass.http.register_view(BCSReadmeView(core))
hass.http.register_view(BCSVersionsView(core))
hass.http.register_view(BCSReleaseNotesView(core))
hass.http.register_view(BCSRepoDetailView(core))
hass.http.register_view(BCSCustomRepoView(core))
hass.http.register_view(BCSInstallView(core))
hass.http.register_view(BCSUpdateView(core))
hass.http.register_view(BCSUninstallView(core))
hass.http.register_view(BCSBackupsView(core))
hass.http.register_view(BCSRestoreView(core))
hass.http.register_view(BCSRestartView(core))
await async_register_panel(
@@ -56,17 +90,24 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
frontend_url_path="bahmcloud-store",
webcomponent_name="bahmcloud-store-panel",
# IMPORTANT: bump v to avoid caching old JS
module_url="/api/bahmcloud_store_static/panel.js?v=102",
module_url="/api/bahmcloud_store_static/panel.js?v=112",
sidebar_title="Bahmcloud Store",
sidebar_icon="mdi:store",
require_admin=True,
config={},
)
try:
await core.full_refresh(source="startup")
except BCSError as e:
_LOGGER.error("Initial refresh failed: %s", e)
async def _do_startup_refresh(_now=None) -> None:
try:
await core.full_refresh(source="startup")
except BCSError as e:
_LOGGER.error("Initial refresh failed: %s", e)
# Do not block startup; refresh after HA is up.
def _on_ha_started(_event) -> None:
async_call_later(hass, 30, _do_startup_refresh)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _on_ha_started)
async def periodic(_now) -> None:
try:
@@ -79,4 +120,16 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
interval_seconds = int(getattr(core, "refresh_seconds", 300) or 300)
async_track_time_interval(hass, periodic, timedelta(seconds=interval_seconds))
return True
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
try:
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
except Exception:
pass
return unload_ok

View File

@@ -0,0 +1,71 @@
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from .const import CONF_GITHUB_TOKEN, DOMAIN
_LOGGER = logging.getLogger(__name__)
def _schema(default_token: str | None = None) -> vol.Schema:
default_token = (default_token or "").strip()
return vol.Schema({vol.Optional(CONF_GITHUB_TOKEN, default=default_token): str})
class BahmcloudStoreConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Bahmcloud Store.
The store index URL is fixed and not user-configurable.
The only optional setting is a GitHub token to increase API rate limits.
"""
VERSION = 1
async def async_step_user(self, user_input: dict | None = None):
# Allow only one instance.
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
if user_input is None:
return self.async_show_form(step_id="user", data_schema=_schema(None))
token = str(user_input.get(CONF_GITHUB_TOKEN, "")).strip() or None
return self.async_create_entry(
title="Bahmcloud Store",
data={},
options={CONF_GITHUB_TOKEN: token} if token else {},
)
@staticmethod
@callback
def async_get_options_flow(config_entry: config_entries.ConfigEntry):
return BahmcloudStoreOptionsFlowHandler(config_entry)
class BahmcloudStoreOptionsFlowHandler(config_entries.OptionsFlow):
"""Options flow to manage optional GitHub token."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self._config_entry = config_entry
async def async_step_init(self, user_input: dict | None = None):
if user_input is None:
current = self._config_entry.options.get(CONF_GITHUB_TOKEN) or ""
return self.async_show_form(step_id="init", data_schema=_schema(str(current)))
token = str(user_input.get(CONF_GITHUB_TOKEN, "")).strip() or None
options = dict(self._config_entry.options)
# Allow clearing the token.
if token:
options[CONF_GITHUB_TOKEN] = token
else:
options.pop(CONF_GITHUB_TOKEN, None)
return self.async_create_entry(title="", data=options)

View File

@@ -0,0 +1,11 @@
"""Constants for Bahmcloud Store."""
from __future__ import annotations
DOMAIN = "bahmcloud_store"
# Fixed store index URL (not user-configurable).
DEFAULT_STORE_URL = "https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml"
# Config entry option keys
CONF_GITHUB_TOKEN = "github_token"

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{
"domain": "bahmcloud_store",
"name": "Bahmcloud Store",
"version": "0.5.9",
"version": "0.7.6",
"documentation": "https://git.bahmcloud.de/bahmcloud/bahmcloud_store",
"config_flow": true,
"platforms": ["update"],
"requirements": [],
"codeowners": ["@bahmcloud"],
"iot_class": "local_polling"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -189,9 +189,11 @@ async def _github_latest_version_redirect(hass: HomeAssistant, owner: str, repo:
return None, None
async def _github_latest_version_api(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]:
async def _github_latest_version_api(
hass: HomeAssistant, owner: str, repo: str, *, github_token: str | None = None
) -> tuple[str | None, str | None]:
session = async_get_clientsession(hass)
headers = {"Accept": "application/vnd.github+json", "User-Agent": UA}
headers = _github_headers(github_token)
data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers)
if isinstance(data, dict) and data.get("tag_name"):
@@ -217,12 +219,14 @@ async def _github_latest_version_api(hass: HomeAssistant, owner: str, repo: str)
return None, None
async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]:
async def _github_latest_version(
hass: HomeAssistant, owner: str, repo: str, *, github_token: str | None = None
) -> tuple[str | None, str | None]:
tag, src = await _github_latest_version_redirect(hass, owner, repo)
if tag:
return tag, src
tag, src = await _github_latest_version_api(hass, owner, repo)
tag, src = await _github_latest_version_api(hass, owner, repo, github_token=github_token)
if tag:
return tag, src
@@ -316,7 +320,16 @@ async def _gitlab_latest_version(
return None, None
async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo:
def _github_headers(github_token: str | None = None) -> dict:
headers = {"Accept": "application/vnd.github+json", "User-Agent": UA}
token = (github_token or "").strip()
if token:
# PAT or fine-grained token
headers["Authorization"] = f"Bearer {token}"
return headers
async def fetch_repo_info(hass: HomeAssistant, repo_url: str, *, github_token: str | None = None) -> RepoInfo:
provider = detect_provider(repo_url)
owner, repo = _split_owner_repo(repo_url)
@@ -337,7 +350,7 @@ async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo:
try:
if provider == "github":
headers = {"Accept": "application/vnd.github+json", "User-Agent": UA}
headers = _github_headers(github_token)
data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}", headers=headers)
if isinstance(data, dict):
@@ -356,7 +369,7 @@ async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo:
if desc:
info.description = desc
ver, src = await _github_latest_version(hass, owner, repo)
ver, src = await _github_latest_version(hass, owner, repo, github_token=github_token)
info.latest_version = ver
info.latest_version_source = src
return info
@@ -504,4 +517,239 @@ async def fetch_readme_markdown(
except Exception:
continue
return None
return None
async def fetch_repo_versions(
hass: HomeAssistant,
repo_url: str,
*,
provider: str | None = None,
default_branch: str | None = None,
limit: int = 20,
github_token: str | None = None,
) -> list[dict[str, str]]:
"""List available versions/refs for a repository.
Returns a list of dicts with keys:
- ref: the ref to install (tag/release/branch)
- label: human-friendly label
- source: release|tag|branch
Notes:
- Uses provider APIs; for GitHub we include the configured token (if any) to avoid unauthenticated rate limits.
- We prefer releases first (if available), then tags.
"""
repo_url = (repo_url or "").strip()
if not repo_url:
return []
prov = (provider or "").strip().lower() if provider else ""
if not prov:
prov = detect_provider(repo_url)
owner, repo = _split_owner_repo(repo_url)
if not owner or not repo:
return []
session = async_get_clientsession(hass)
headers = {"User-Agent": UA}
if prov == "github":
headers = _github_headers(github_token)
out: list[dict[str, str]] = []
seen: set[str] = set()
def _add(ref: str | None, label: str, source: str) -> None:
r = (ref or "").strip()
if not r or r in seen:
return
seen.add(r)
out.append({"ref": r, "label": label, "source": source})
# Always offer default branch as an explicit option.
if default_branch and str(default_branch).strip():
b = str(default_branch).strip()
_add(b, f"Branch: {b}", "branch")
try:
if prov == "github":
# Releases (prefer these over tags)
# Use the configured GitHub token (if any) to avoid unauthenticated rate limits.
gh_headers = _github_headers(github_token)
per_page = max(1, min(int(limit), 100))
data, _ = await _safe_json(
session,
f"https://api.github.com/repos/{owner}/{repo}/releases?per_page={per_page}",
headers=gh_headers,
)
if isinstance(data, list):
for r in data:
if not isinstance(r, dict):
continue
tag = r.get("tag_name")
name = r.get("name")
if tag:
lbl = str(tag)
if isinstance(name, str) and name.strip() and name.strip() != str(tag):
lbl = f"{tag}{name.strip()}"
_add(str(tag), lbl, "release")
# Tags
data, _ = await _safe_json(
session,
f"https://api.github.com/repos/{owner}/{repo}/tags?per_page={per_page}",
headers=gh_headers,
)
if isinstance(data, list):
for t in data:
if isinstance(t, dict) and t.get("name"):
_add(str(t["name"]), str(t["name"]), "tag")
return out
if prov == "gitlab":
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"
project = quote_plus(f"{owner}/{repo}")
data, _ = await _safe_json(
session,
f"{base}/api/v4/projects/{project}/releases?per_page={int(limit)}",
headers=headers,
)
if isinstance(data, list):
for r in data:
if not isinstance(r, dict):
continue
tag = r.get("tag_name")
name = r.get("name")
if tag:
lbl = str(tag)
if isinstance(name, str) and name.strip() and name.strip() != str(tag):
lbl = f"{tag}{name.strip()}"
_add(str(tag), lbl, "release")
data, _ = await _safe_json(
session,
f"{base}/api/v4/projects/{project}/repository/tags?per_page={int(limit)}",
headers=headers,
)
if isinstance(data, list):
for t in data:
if isinstance(t, dict) and t.get("name"):
_add(str(t["name"]), str(t["name"]), "tag")
return out
# gitea (incl. Bahmcloud)
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"
data, _ = await _safe_json(
session,
f"{base}/api/v1/repos/{owner}/{repo}/releases?limit={int(limit)}",
headers=headers,
)
if isinstance(data, list):
for r in data:
if not isinstance(r, dict):
continue
tag = r.get("tag_name")
name = r.get("name")
if tag:
lbl = str(tag)
if isinstance(name, str) and name.strip() and name.strip() != str(tag):
lbl = f"{tag}{name.strip()}"
_add(str(tag), lbl, "release")
data, _ = await _safe_json(
session,
f"{base}/api/v1/repos/{owner}/{repo}/tags?limit={int(limit)}",
headers=headers,
)
if isinstance(data, list):
for t in data:
if isinstance(t, dict) and t.get("name"):
_add(str(t["name"]), str(t["name"]), "tag")
return out
except Exception:
_LOGGER.debug("fetch_repo_versions failed for %s", repo_url, exc_info=True)
return out
async def fetch_release_notes_markdown(
hass: HomeAssistant,
repo_url: str,
*,
ref: str | None,
provider: str | None = None,
github_token: str | None = None,
) -> str | None:
"""Fetch release notes for a specific release tag."""
repo_url = (repo_url or "").strip()
target_ref = (ref or "").strip()
if not repo_url or not target_ref:
return None
prov = (provider or "").strip().lower() if provider else ""
if not prov:
prov = detect_provider(repo_url)
owner, repo = _split_owner_repo(repo_url)
if not owner or not repo:
return None
session = async_get_clientsession(hass)
try:
if prov == "github":
data, status = await _safe_json(
session,
f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{quote_plus(target_ref)}",
headers=_github_headers(github_token),
)
if status == 200 and isinstance(data, dict):
body = data.get("body")
if isinstance(body, str) and body.strip():
return body
return None
if prov == "gitlab":
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"
project = quote_plus(f"{owner}/{repo}")
data, status = await _safe_json(
session,
f"{base}/api/v4/projects/{project}/releases/{quote_plus(target_ref)}",
headers={"User-Agent": UA},
)
if status == 200 and isinstance(data, dict):
body = data.get("description")
if isinstance(body, str) and body.strip():
return body
return None
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"
data, status = await _safe_json(
session,
f"{base}/api/v1/repos/{owner}/{repo}/releases/tags/{quote_plus(target_ref)}",
headers={"User-Agent": UA},
)
if status == 200 and isinstance(data, dict):
body = data.get("body")
if isinstance(body, str) and body.strip():
return body
note = data.get("note")
if isinstance(note, str) and note.strip():
return note
return None
except Exception:
_LOGGER.debug("fetch_release_notes_markdown failed for %s ref=%s", repo_url, target_ref, exc_info=True)
return None

View File

@@ -25,6 +25,8 @@ class InstalledRepo:
url: str
domains: list[str]
installed_at: int
install_type: str = "integration"
installed_paths: list[str] | None = None
installed_version: str | None = None # BCS "installed ref" (tag/release/branch)
installed_manifest_version: str | None = None # informational only
ref: str | None = None # kept for backward compatibility / diagnostics
@@ -36,6 +38,9 @@ class BCSStorage:
Keys:
- custom_repos: list of manually added repositories
- installed_repos: mapping repo_id -> installed metadata
- settings: persistent user settings (e.g. toggles in the UI)
- hacs_cache: cached HACS metadata to improve UX (display names/descriptions)
- repo_cache: cached per-repo enrichment (names/descriptions/versions) to keep the UI populated after restart
"""
def __init__(self, hass: HomeAssistant) -> None:
@@ -53,8 +58,92 @@ class BCSStorage:
if "installed_repos" not in data or not isinstance(data.get("installed_repos"), dict):
data["installed_repos"] = {}
if "settings" not in data or not isinstance(data.get("settings"), dict):
data["settings"] = {}
if "hacs_cache" not in data or not isinstance(data.get("hacs_cache"), dict):
data["hacs_cache"] = {}
if "repo_cache" not in data or not isinstance(data.get("repo_cache"), dict):
data["repo_cache"] = {}
return data
async def get_repo_cache(self) -> dict[str, Any]:
"""Return cached per-repo enrichment data.
Shape:
{
"fetched_at": <unix_ts>,
"repos": {
"<repo_id>": {
"ts": <unix_ts>,
"url": "...",
"name": "...",
"provider_description": "...",
"meta_name": "...",
"meta_description": "...",
"meta_category": "...",
"meta_source": "...",
"latest_version": "...",
"latest_version_source": "...",
"default_branch": "...",
"owner": "...",
"provider_repo_name": "..."
}
}
}
"""
data = await self._load()
cache = data.get("repo_cache", {})
return cache if isinstance(cache, dict) else {}
async def set_repo_cache(self, cache: dict[str, Any]) -> None:
"""Persist cached per-repo enrichment data."""
data = await self._load()
data["repo_cache"] = cache if isinstance(cache, dict) else {}
await self._save(data)
async def get_hacs_cache(self) -> dict[str, Any]:
"""Return cached HACS metadata.
Shape:
{
"fetched_at": <unix_ts>,
"repos": {"owner/repo": {"name": "...", "description": "...", "domain": "..."}}
}
"""
data = await self._load()
cache = data.get("hacs_cache", {})
return cache if isinstance(cache, dict) else {}
async def set_hacs_cache(self, cache: dict[str, Any]) -> None:
"""Persist cached HACS metadata."""
data = await self._load()
data["hacs_cache"] = cache if isinstance(cache, dict) else {}
await self._save(data)
async def get_settings(self) -> dict[str, Any]:
"""Return persistent settings.
Currently used for UI/behavior toggles.
"""
data = await self._load()
settings = data.get("settings", {})
return settings if isinstance(settings, dict) else {}
async def set_settings(self, updates: dict[str, Any]) -> dict[str, Any]:
"""Update persistent settings and return the merged settings."""
data = await self._load()
settings = data.get("settings", {})
if not isinstance(settings, dict):
settings = {}
for k, v in (updates or {}).items():
settings[str(k)] = v
data["settings"] = settings
await self._save(data)
return settings
async def _save(self, data: dict[str, Any]) -> None:
await self._store.async_save(data)
@@ -110,6 +199,11 @@ class BCSStorage:
if not isinstance(domains, list):
domains = []
domains = [str(d) for d in domains if str(d).strip()]
installed_paths = entry.get("installed_paths") or []
if not isinstance(installed_paths, list):
installed_paths = []
installed_paths = [str(p) for p in installed_paths if str(p).strip()]
install_type = str(entry.get("install_type") or "integration").strip() or "integration"
installed_version = entry.get("installed_version")
ref = entry.get("ref")
@@ -126,6 +220,8 @@ class BCSStorage:
url=str(entry.get("url") or ""),
domains=domains,
installed_at=int(entry.get("installed_at") or 0),
install_type=install_type,
installed_paths=installed_paths,
installed_version=str(installed_version) if installed_version else None,
installed_manifest_version=str(installed_manifest_version) if installed_manifest_version else None,
ref=str(ref) if ref else None,
@@ -151,9 +247,11 @@ class BCSStorage:
repo_id: str,
url: str,
domains: list[str],
installed_version: str | None,
installed_version: str | None = None,
installed_manifest_version: str | None = None,
ref: str | None,
ref: str | None = None,
install_type: str = "integration",
installed_paths: list[str] | None = None,
) -> None:
data = await self._load()
installed = data.get("installed_repos", {})
@@ -165,6 +263,8 @@ class BCSStorage:
"repo_id": str(repo_id),
"url": str(url),
"domains": [str(d) for d in (domains or []) if str(d).strip()],
"install_type": str(install_type or "integration"),
"installed_paths": [str(p) for p in (installed_paths or []) if str(p).strip()],
"installed_at": int(time.time()),
# IMPORTANT: this is what BCS uses as "installed version" (ref/tag/branch)
"installed_version": installed_version,
@@ -181,4 +281,4 @@ class BCSStorage:
if isinstance(installed, dict) and repo_id in installed:
installed.pop(repo_id, None)
data["installed_repos"] = installed
await self._save(data)
await self._save(data)

View File

@@ -1,4 +1,29 @@
{
"config": {
"abort": {
"single_instance_allowed": "Only one Bahmcloud Store instance can be configured."
},
"step": {
"user": {
"title": "Bahmcloud Store",
"description": "Bahmcloud Store uses a fixed official store index. You can optionally add a GitHub token to increase API rate limits.",
"data": {
"github_token": "GitHub token (optional)"
}
}
}
},
"options": {
"step": {
"init": {
"title": "Bahmcloud Store Options",
"description": "Optionally set or clear your GitHub token to reduce rate limiting.",
"data": {
"github_token": "GitHub token (optional)"
}
}
}
},
"issues": {
"restart_required": {
"title": "Restart required",
@@ -15,4 +40,4 @@
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import inspect
import logging
from dataclasses import dataclass
from typing import Any
@@ -9,12 +10,23 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers import entity_registry as er
from .core import DOMAIN, SIGNAL_UPDATED, BCSCore
from .const import DOMAIN
from .core import SIGNAL_UPDATED, BCSCore
_LOGGER = logging.getLogger(__name__)
def _get_core(hass: HomeAssistant) -> BCSCore | None:
data = hass.data.get(DOMAIN)
if isinstance(data, dict):
c = data.get("_core")
return c if isinstance(c, BCSCore) else None
# Backwards compatibility (older versions used hass.data[DOMAIN] = core)
return data if isinstance(data, BCSCore) else None
def _pretty_repo_name(core: BCSCore, repo_id: str) -> str:
"""Return a human-friendly name for a repo update entity."""
try:
@@ -117,8 +129,36 @@ class BCSRepoUpdateEntity(UpdateEntity):
def _sync_entities(core: BCSCore, existing: dict[str, BCSRepoUpdateEntity], async_add_entities: AddEntitiesCallback) -> None:
"""Ensure there is one update entity per installed repo AND keep names in sync."""
installed_map = getattr(core, "_installed_cache", {}) or {}
installed_repo_ids = {repo_id for repo_id, data in installed_map.items() if isinstance(data, dict)}
new_entities: list[BCSRepoUpdateEntity] = []
stale_repo_ids = [repo_id for repo_id in list(existing) if repo_id not in installed_repo_ids]
if stale_repo_ids:
registry = er.async_get(core.hass)
for repo_id in stale_repo_ids:
ent = existing.pop(repo_id, None)
if not ent:
continue
try:
entity_id = getattr(ent, "entity_id", None)
if not entity_id:
for entry in registry.entities.values():
if getattr(entry, "platform", None) == DOMAIN and getattr(entry, "unique_id", None) == ent.unique_id:
entity_id = entry.entity_id
break
if entity_id:
registry.async_remove(entity_id)
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("BCS could not remove stale update entity for %s: %s", repo_id, exc)
try:
result = ent.async_remove()
if inspect.isawaitable(result):
core.hass.async_create_task(result)
except Exception as exc: # pylint: disable=broad-exception-caught
_LOGGER.debug("BCS could not detach stale update entity for %s: %s", repo_id, exc)
for repo_id, data in installed_map.items():
if not isinstance(data, dict):
continue
@@ -146,7 +186,7 @@ async def async_setup_platform(
discovery_info=None,
):
"""Set up BCS update entities."""
core: BCSCore | None = hass.data.get(DOMAIN)
core: BCSCore | None = _get_core(hass)
if not core:
_LOGGER.debug("BCS core not available, skipping update platform setup")
return
@@ -159,4 +199,13 @@ async def async_setup_platform(
def _handle_update() -> None:
_sync_entities(core, entities, async_add_entities)
async_dispatcher_connect(hass, SIGNAL_UPDATED, _handle_update)
async_dispatcher_connect(hass, SIGNAL_UPDATED, _handle_update)
async def async_setup_entry(
hass: HomeAssistant,
entry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up BCS update entities from a config entry."""
await async_setup_platform(hass, {}, async_add_entities, None)

View File

@@ -215,7 +215,12 @@ class BCSApiView(HomeAssistantView):
async def get(self, request: web.Request) -> web.Response:
return web.json_response(
{"ok": True, "version": self.core.version, "repos": self.core.list_repos_public()}
{
"ok": True,
"version": self.core.version,
"settings": self.core.get_settings_public(),
"repos": self.core.list_repos_public(),
}
)
async def post(self, request: web.Request) -> web.Response:
@@ -248,6 +253,40 @@ class BCSApiView(HomeAssistantView):
return web.json_response({"ok": False, "message": "Unknown operation"}, status=400)
class BCSSettingsView(HomeAssistantView):
"""Persistent UI settings (e.g. toggles)."""
url = "/api/bcs/settings"
name = "api:bcs_settings"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def get(self, request: web.Request) -> web.Response:
return web.json_response({"ok": True, "settings": self.core.get_settings_public()})
async def post(self, request: web.Request) -> web.Response:
try:
data = await request.json()
except Exception:
data = {}
updates: dict[str, Any] = {}
if "hacs_enabled" in data:
updates["hacs_enabled"] = bool(data.get("hacs_enabled"))
if "favorite_repo_ids" in data:
raw = data.get("favorite_repo_ids") or []
updates["favorite_repo_ids"] = raw if isinstance(raw, list) else []
try:
settings = await self.core.set_settings(updates)
return web.json_response({"ok": True, "settings": settings})
except Exception as e:
_LOGGER.exception("BCS set settings failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "Failed"}, status=500)
class BCSCustomRepoView(HomeAssistantView):
url = "/api/bcs/custom_repo"
name = "api:bcs_custom_repo"
@@ -292,6 +331,62 @@ class BCSReadmeView(HomeAssistantView):
return web.json_response({"ok": True, "readme": md_str, "html": html})
class BCSVersionsView(HomeAssistantView):
url = "/api/bcs/versions"
name = "api:bcs_versions"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def get(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
try:
versions = await self.core.list_repo_versions(repo_id)
return web.json_response({"ok": True, "repo_id": repo_id, "versions": versions}, status=200)
except Exception as e:
_LOGGER.exception("BCS list versions failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "List versions failed"}, status=500)
class BCSReleaseNotesView(HomeAssistantView):
url = "/api/bcs/release_notes"
name = "api:bcs_release_notes"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def get(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
ref = request.query.get("ref")
ref = str(ref).strip() if ref is not None else None
try:
notes = await self.core.fetch_release_notes_markdown(repo_id, ref=ref)
if not notes or not str(notes).strip():
return web.json_response(
{"ok": False, "message": "Release notes not found for this version."},
status=404,
)
notes_str = str(notes)
html = _render_markdown_server_side(notes_str)
return web.json_response(
{"ok": True, "ref": ref, "release_notes": notes_str, "html": html},
status=200,
)
except Exception as e:
_LOGGER.exception("BCS release notes failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "Release notes failed"}, status=500)
class BCSInstallView(HomeAssistantView):
url = "/api/bcs/install"
name = "api:bcs_install"
@@ -302,11 +397,13 @@ class BCSInstallView(HomeAssistantView):
async def post(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
version = request.query.get("version")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
try:
result = await self.core.install_repo(repo_id)
v = str(version).strip() if version is not None else None
result = await self.core.install_repo(repo_id, version=v)
return web.json_response(result, status=200)
except Exception as e:
_LOGGER.exception("BCS install failed: %s", e)
@@ -323,11 +420,13 @@ class BCSUpdateView(HomeAssistantView):
async def post(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
version = request.query.get("version")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
try:
result = await self.core.update_repo(repo_id)
v = str(version).strip() if version is not None else None
result = await self.core.update_repo(repo_id, version=v)
return web.json_response(result, status=200)
except Exception as e:
_LOGGER.exception("BCS update failed: %s", e)
@@ -355,6 +454,53 @@ class BCSUninstallView(HomeAssistantView):
return web.json_response({"ok": False, "message": str(e) or "Uninstall failed"}, status=500)
class BCSBackupsView(HomeAssistantView):
url = "/api/bcs/backups"
name = "api:bcs_backups"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def get(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
try:
backups = await self.core.list_repo_backups(repo_id)
return web.json_response({"ok": True, "repo_id": repo_id, "backups": backups}, status=200)
except Exception as e:
_LOGGER.exception("BCS list backups failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "List backups failed"}, status=500)
class BCSRestoreView(HomeAssistantView):
url = "/api/bcs/restore"
name = "api:bcs_restore"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def post(self, request: web.Request) -> web.Response:
repo_id = request.query.get("repo_id")
backup_id = request.query.get("backup_id")
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
if not backup_id:
return web.json_response({"ok": False, "message": "Missing backup_id"}, status=400)
try:
result = await self.core.restore_repo_backup(repo_id, backup_id)
return web.json_response(result, status=200)
except Exception as e:
_LOGGER.exception("BCS restore failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "Restore failed"}, status=500)
class BCSRestartView(HomeAssistantView):
url = "/api/bcs/restart"
name = "api:bcs_restart"
@@ -369,4 +515,64 @@ class BCSRestartView(HomeAssistantView):
return web.json_response({"ok": True})
except Exception as e:
_LOGGER.exception("BCS restart failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "Restart failed"}, status=500)
return web.json_response({"ok": False, "message": str(e) or "Restart failed"}, status=500)
class BCSRepoDetailView(HomeAssistantView):
url = "/api/bcs/repo"
name = "api:bcs_repo"
requires_auth = True
def __init__(self, core: Any) -> None:
self.core: BCSCore = core
async def get(self, request: web.Request) -> web.Response:
repo_id = (request.query.get("repo_id") or "").strip()
if not repo_id:
return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
try:
repo = await self.core.ensure_repo_details(repo_id)
if not repo:
return web.json_response({"ok": False, "message": "Repo not found"}, status=404)
inst = self.core.get_installed(repo_id) or {}
installed = bool(inst)
domains = inst.get("domains") or []
if not isinstance(domains, list):
domains = []
installed_paths = inst.get("installed_paths") or []
if not isinstance(installed_paths, list):
installed_paths = []
return web.json_response({
"ok": True,
"repo": {
"id": repo.id,
"name": repo.name,
"url": repo.url,
"source": repo.source,
"owner": repo.owner,
"provider": repo.provider,
"repo_name": repo.provider_repo_name,
"description": repo.provider_description or repo.meta_description,
"default_branch": repo.default_branch,
"latest_version": repo.latest_version,
"latest_version_source": repo.latest_version_source,
"category": repo.meta_category,
"category_key": self.core._repo_install_type(repo),
"install_target": self.core._repo_install_target(repo),
"meta_author": repo.meta_author,
"meta_maintainer": repo.meta_maintainer,
"meta_source": repo.meta_source,
"installed": installed,
"install_type": inst.get("install_type") or self.core._repo_install_type(repo),
"installed_version": inst.get("installed_version"),
"installed_manifest_version": inst.get("installed_manifest_version"),
"installed_domains": domains,
"installed_paths": installed_paths,
"favorite": self.core.is_favorite_repo(repo.id),
}
}, status=200)
except Exception as e:
_LOGGER.exception("BCS repo details failed: %s", e)
return web.json_response({"ok": False, "message": str(e) or "Repo details failed"}, status=500)