Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a55281938c | |||
| 48f8ef6265 |
4
.idea/changes.md
generated
4
.idea/changes.md
generated
@@ -15,6 +15,10 @@
|
|||||||
- Added a persistent language rule that all project-facing repository content must stay in English regardless of the chat language.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
### Documented
|
### 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.
|
- 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.
|
||||||
|
|||||||
15
.idea/start prompt.md
generated
15
.idea/start prompt.md
generated
@@ -6,6 +6,7 @@ Project identity:
|
|||||||
- This is a Home Assistant custom integration named `bahmcloud_store`.
|
- This is a Home Assistant custom integration named `bahmcloud_store`.
|
||||||
- The product goal is a provider-neutral store for Home Assistant custom integrations, similar in spirit to HACS, but not limited to GitHub.
|
- The product goal is a provider-neutral store for Home Assistant custom integrations, similar in spirit to HACS, but not limited to GitHub.
|
||||||
- Current real provider implementation is strongest for GitHub, GitLab, and Gitea-compatible providers. Unknown providers currently fall through the Gitea-style code paths, so do not assume every arbitrary Git provider works without verification.
|
- Current real provider implementation is strongest for GitHub, GitLab, and Gitea-compatible providers. Unknown providers currently fall through the Gitea-style code paths, so do not assume every arbitrary Git provider works without verification.
|
||||||
|
- The long-term product direction is broader than integrations only: the store should evolve into a Home Assistant content store that can manage integrations, blueprints, templates, Lovelace designs, and additional future categories from multiple git providers.
|
||||||
|
|
||||||
Working rules:
|
Working rules:
|
||||||
- All project-facing work must be done in English only, regardless of the language the user speaks in chat. This applies to code comments, documentation, changelog entries, release notes, commit messages, PR text, UI text, issue text, and any other project artifacts unless the user explicitly requests a specific exception for repository content.
|
- All project-facing work must be done in English only, regardless of the language the user speaks in chat. This applies to code comments, documentation, changelog entries, release notes, commit messages, PR text, UI text, issue text, and any other project artifacts unless the user explicitly requests a specific exception for repository content.
|
||||||
@@ -102,6 +103,16 @@ Code-analysis findings that should influence future work:
|
|||||||
- The end-user and full READMEs contain some stale or inconsistent details compared with the current UI and code. Verify behavior in source before using README text as specification.
|
- The end-user and full READMEs contain some stale or inconsistent details compared with the current UI and code. Verify behavior in source before using README text as specification.
|
||||||
- There are visible encoding/mojibake issues in some documentation and older UI assets. Preserve valid UTF-8 when editing.
|
- There are visible encoding/mojibake issues in some documentation and older UI assets. Preserve valid UTF-8 when editing.
|
||||||
|
|
||||||
|
Planned product expansion:
|
||||||
|
1. Add support for Home Assistant blueprints and install them directly into the correct Home Assistant blueprint location from the store.
|
||||||
|
2. Add templates and Lovelace designs to the store so they can be discovered and installed from the same UI.
|
||||||
|
3. Add support for more categories beyond integrations and design the architecture so category-specific install targets and validation rules are explicit.
|
||||||
|
|
||||||
|
Implications for future architecture:
|
||||||
|
- The current install pipeline is integration-centric because it assumes repository content under `custom_components/`.
|
||||||
|
- Future category support should move toward category-aware install strategies instead of one universal install path.
|
||||||
|
- Store metadata and index entries will likely need stronger category typing, install-target definitions, and validation rules per category.
|
||||||
|
|
||||||
Project constraints to respect in future edits:
|
Project constraints to respect in future edits:
|
||||||
- Keep async I/O non-blocking in Home Assistant.
|
- Keep async I/O non-blocking in Home Assistant.
|
||||||
- Avoid startup-heavy network work before HA is fully started.
|
- Avoid startup-heavy network work before HA is fully started.
|
||||||
@@ -119,5 +130,5 @@ Recommended workflow for future tasks:
|
|||||||
6. If and only if a release is being prepared with user approval, fold release-worthy changes into `CHANGELOG.md`.
|
6. If and only if a release is being prepared with user approval, fold release-worthy changes into `CHANGELOG.md`.
|
||||||
|
|
||||||
Current release baseline:
|
Current release baseline:
|
||||||
- `manifest.json` version is `0.7.4`
|
- `manifest.json` version is `0.7.5`
|
||||||
- Latest documented release in `CHANGELOG.md` is `0.7.4` dated `2026-03-23`
|
- Latest documented release in `CHANGELOG.md` is `0.7.5` dated `2026-03-23`
|
||||||
|
|||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -11,6 +11,18 @@ Sections:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## 0.7.4 - 2026-03-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
74
README.md
74
README.md
@@ -1,50 +1,86 @@
|
|||||||
# Bahmcloud Store (BCS) for Home Assistant
|
# Bahmcloud Store (BCS) for Home Assistant
|
||||||
|
|
||||||
Bahmcloud Store (BCS) is a provider-neutral store for Home Assistant custom integrations.
|
Bahmcloud Store (BCS) is a provider-neutral store for Home Assistant content hosted on git platforms.
|
||||||
It allows you to browse, install, update, downgrade, uninstall and restore integrations
|
It currently supports installing and managing:
|
||||||
directly from the Home Assistant UI.
|
|
||||||
|
|
||||||
This README is intended for **end users**.
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Option A: Home Assistant OS / Supervised (Add-on – recommended)
|
### Option A: Home Assistant OS / Supervised
|
||||||
|
|
||||||
1. Open **Settings → Add-ons → Add-on Store**
|
1. Open **Settings -> Add-ons -> Add-on Store**
|
||||||
2. Add the Bahmcloud Add-on repository
|
2. Add the Bahmcloud add-on repository:
|
||||||
`https://git.bahmcloud.de/bahmcloud/addons`
|
`https://git.bahmcloud.de/bahmcloud/addons`
|
||||||
3. Install **Bahmcloud Store Installer**
|
3. Install **Bahmcloud Store Installer**
|
||||||
4. Start the add-on
|
4. Start the add-on
|
||||||
5. Restart Home Assistant
|
5. Restart Home Assistant
|
||||||
|
|
||||||
Installed to:
|
Installed to:
|
||||||
/config/custom_components/bahmcloud_store
|
`/config/custom_components/bahmcloud_store`
|
||||||
|
|
||||||
---
|
### Option B: Manual Installation
|
||||||
|
|
||||||
### Option B: Manual Installation (Container / Core)
|
|
||||||
|
|
||||||
1. Download the latest release
|
1. Download the latest release
|
||||||
2. Copy `custom_components/bahmcloud_store` to:
|
2. Copy `custom_components/bahmcloud_store` to:
|
||||||
/config/custom_components/bahmcloud_store
|
`/config/custom_components/bahmcloud_store`
|
||||||
3. Restart Home Assistant
|
3. Restart Home Assistant
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Repository Sources
|
## Repository Sources
|
||||||
|
|
||||||
- **BCS Official**
|
- **BCS Official**: repositories from the Bahmcloud store index
|
||||||
- **HACS**
|
- **HACS**: optional official HACS integration source
|
||||||
- **Custom**
|
- **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 and uninstall through the store
|
||||||
|
- No restart is normally required for blueprint deployment
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Developer documentation:
|
- Developer documentation: `README_DEVELOPER.md`
|
||||||
https://git.bahmcloud.de/bahmcloud/bahmcloud_store/src/branch/main/README_DEVELOPER.md
|
- Full user guide: `README_FULL.md`
|
||||||
|
|
||||||
Full technical documentation:
|
|
||||||
https://git.bahmcloud.de/bahmcloud/bahmcloud_store/src/branch/main/README_FULL.md
|
|
||||||
|
|||||||
@@ -1,115 +1,159 @@
|
|||||||
# Bahmcloud Store – Developer Documentation
|
# Bahmcloud Store - Developer Documentation
|
||||||
|
|
||||||
For contributors and maintainers.
|
For contributors and maintainers.
|
||||||
|
|
||||||
## Architecture
|
## Project Scope
|
||||||
|
|
||||||
Repositories:
|
Bahmcloud Store is evolving from an integration-only store into a broader Home Assistant git-based content store.
|
||||||
1) Installer Add-on (HAOS/Supervised) ```https://git.bahmcloud.de/bahmcloud/addons```
|
|
||||||
2) Core Integration ```https://git.bahmcloud.de/bahmcloud/bahmcloud_store```
|
|
||||||
3) Store Index (`store.yaml`) ```https://git.bahmcloud.de/bahmcloud/ha_store```
|
|
||||||
|
|
||||||
### Integration Layout
|
Currently supported install categories:
|
||||||
|
|
||||||
custom_components/bahmcloud_store/
|
- Integrations
|
||||||
- __init__.py: setup, panel registration, schedule background after HA started
|
- Blueprints
|
||||||
- core.py: index merge, enrichment, install/update/uninstall, backups, restore, caching
|
|
||||||
- providers.py: GitHub/GitLab/Gitea repo info + latest version helpers
|
Planned categories:
|
||||||
- metadata.py: read bcs.yaml / hacs.json / hacs.yaml
|
|
||||||
- storage.py: persistent storage (installed, custom, repo cache, hacs cache)
|
- Templates
|
||||||
- views.py: HTTP API endpoints
|
- Lovelace / dashboard designs
|
||||||
- update.py: UpdateEntity implementation
|
- Additional content types with category-specific install logic
|
||||||
- repairs.py: (optional) Repairs flow for restart
|
|
||||||
- panel/: UI (panel.js, styles.css, etc.)
|
## Repository Layout
|
||||||
- manifest.json
|
|
||||||
|
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
|
## Runtime Model
|
||||||
|
|
||||||
- RepoItem (merged)
|
- `RepoItem`: merged repository model used by the UI and backend
|
||||||
- Installed repos (storage)
|
- Installed repositories: persisted in Home Assistant storage
|
||||||
- Repo cache (persisted enrichment)
|
- Settings: persistent UI and behavior settings such as HACS enablement and pinned repositories
|
||||||
- HACS meta cache (mapping owner/repo → name/description)
|
- Repo cache: provider and metadata enrichment cache
|
||||||
|
- HACS cache: display metadata cache for HACS integration repositories
|
||||||
## Background
|
|
||||||
|
|
||||||
- Heavy work only after `homeassistant_started`
|
|
||||||
- Refresh: if index unchanged → installed-only refresh + schedule enrichment
|
|
||||||
- Opening a repo triggers `ensure_repo_details()` and persists to cache
|
|
||||||
|
|
||||||
## Providers
|
|
||||||
|
|
||||||
- GitHub: API/releases/tags/atom + raw readme
|
|
||||||
- GitLab: API releases/tags + raw readme
|
|
||||||
- Gitea: API releases/tags + raw readme
|
|
||||||
- Custom: API or HTTPS Request to your Git Provider
|
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
|
|
||||||
- Prefer `bcs.yaml`, fallback `hacs.json` / `hacs.yaml`
|
Metadata priority:
|
||||||
- Populate name/description/category/author/maintainer
|
|
||||||
|
|
||||||
## HTTP API (excerpt)
|
1. `bcs.yaml`
|
||||||
|
2. `hacs.yaml`
|
||||||
|
3. `hacs.json`
|
||||||
|
|
||||||
Base: /api/bcs
|
Common fields:
|
||||||
|
|
||||||
- GET /api/bcs
|
|
||||||
- POST /api/bcs?action=refresh
|
|
||||||
- GET /api/bcs/readme?repo_id=...
|
|
||||||
- GET /api/bcs/versions?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=...
|
|
||||||
- (optional) POST/DELETE custom_repo
|
|
||||||
|
|
||||||
## Update Entities
|
|
||||||
|
|
||||||
- Unique id bcs:<repo_id>
|
|
||||||
- Compare installed ref vs latest ref
|
|
||||||
- Dispatcher signal on refresh/install/update
|
|
||||||
|
|
||||||
## Storage
|
|
||||||
|
|
||||||
- JSON in HA `.storage`
|
|
||||||
- async read/write helpers
|
|
||||||
- repo cache applied on startup
|
|
||||||
|
|
||||||
## Contributing to **BCS Official**
|
|
||||||
|
|
||||||
1) Add pull request to `https://git.bahmcloud.de/bahmcloud/ha_store` (with your integration added to) `store.yaml`)
|
|
||||||
2) Add entry:
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Your Integration Name
|
name: Example Project
|
||||||
url: https://your-git-hoster.com/your-org/your-repo
|
description: Short description
|
||||||
category: Category (actually only "Integrations" are supported)
|
category: Integrations
|
||||||
|
author: Example Author
|
||||||
|
maintainer: Example Maintainer
|
||||||
```
|
```
|
||||||
3) (Recommended) Add `bcs.yaml` to your repo:
|
|
||||||
|
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/...`
|
||||||
|
- Initial support is focused on direct install and uninstall
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Contributing to the Official Store Index
|
||||||
|
|
||||||
|
Add a repository to the BCS store index in `ha_store`.
|
||||||
|
|
||||||
|
Example index entry:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Your Integration Name
|
- name: Example Project
|
||||||
description: One-liner for the store (optional, store information are also catched from git repository)
|
url: https://your-git-hoster.example/org/repo
|
||||||
category: Integrations (actually only supported)
|
category: Blueprint
|
||||||
author: Your Name
|
|
||||||
maintainer: Your Handle
|
|
||||||
```
|
```
|
||||||
4) Open PR; validation checks: reachable, has `custom_components/<domain>/manifest.json`, sensible metadata
|
|
||||||
5) Merge → appears in **BCS Official** after refresh
|
|
||||||
|
|
||||||
## Coding Guidelines
|
Recommended repository metadata:
|
||||||
|
|
||||||
- Async I/O, no blocking event loop
|
```yaml
|
||||||
- Respect provider rate limits
|
name: Example Project
|
||||||
- Clean logging around refresh/install/update/restore
|
description: One-line description
|
||||||
- Keep UI responsive; throttle updates
|
category: Blueprint
|
||||||
|
author: Example Author
|
||||||
|
maintainer: Example Maintainer
|
||||||
|
```
|
||||||
|
|
||||||
---
|
Validation should match the category:
|
||||||
|
|
||||||
## Planed Features
|
- Integrations: verify `custom_components/<domain>/manifest.json`
|
||||||
|
- Blueprints: verify `blueprints/...`
|
||||||
|
|
||||||
- Add Downloads and install for category "Dashboard"
|
## Design Direction
|
||||||
- Add Downloads and install for category "Template"
|
|
||||||
- Add Downloads and install for category "Theme"
|
|
||||||
- Add Downloads and install for category "Blueprint"
|
|
||||||
-
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|||||||
327
README_FULL.md
327
README_FULL.md
@@ -1,27 +1,30 @@
|
|||||||
# Bahmcloud Store – Full User Guide
|
# Bahmcloud Store - Full User Guide
|
||||||
|
|
||||||
This guide explains **all features** of Bahmcloud Store (BCS) for Home Assistant.
|
This guide explains the current feature set of Bahmcloud Store (BCS) for Home Assistant.
|
||||||
It is written for users and admins who want a complete, practical reference.
|
|
||||||
|
|
||||||
> BCS lets you install & manage custom integrations from **GitHub/GitLab/Gitea** and your own sources, with backups, restore, and version pinning.
|
BCS is a git-based store UI that can currently manage integrations and blueprints from multiple repository providers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
- Concepts
|
- Concepts
|
||||||
- Sources (BCS / HACS / Custom)
|
- Sources
|
||||||
|
- Supported Content Types
|
||||||
- UI Overview
|
- UI Overview
|
||||||
- Finding Integrations
|
- Finding Repositories
|
||||||
- Installing
|
- Installing
|
||||||
- Selecting Versions / Downgrading
|
- Selecting Versions
|
||||||
- Updating
|
- Updating
|
||||||
- Uninstalling
|
- Uninstalling
|
||||||
- Backups & Restore
|
- Pinned Repositories
|
||||||
|
- Release Notes
|
||||||
|
- Backups and Restore
|
||||||
- Custom Repositories
|
- Custom Repositories
|
||||||
- HACS Repositories
|
- HACS Repositories
|
||||||
- Update Entities in Home Assistant
|
- Update Entities
|
||||||
- Background Caching & Performance
|
- Performance and Caching
|
||||||
- Restart Required
|
- Restart Behavior
|
||||||
- Troubleshooting
|
- Troubleshooting
|
||||||
- FAQ
|
- FAQ
|
||||||
|
|
||||||
@@ -29,189 +32,281 @@ It is written for users and admins who want a complete, practical reference.
|
|||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
- **Sources**:
|
### Sources
|
||||||
- **BCS Official** → entries from index (`store.yaml`) made and added from Bahmcloud
|
|
||||||
- **HACS** → official HACS integrations list (toggleable)
|
- **BCS Official**: repositories from the official Bahmcloud index
|
||||||
- **Custom** → manual entries you add locally
|
- **HACS**: optional HACS integration source
|
||||||
- **Install location**: `/config/custom_components/<domain>`
|
- **Custom**: repositories you add manually by URL
|
||||||
- **Backup**: BCS keeps pre‑update copies in `/config/.bcs_backups/<domain>/<timestamp>/`
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sources (BCS / HACS / Custom)
|
## Supported Content Types
|
||||||
|
|
||||||
Each repository card shows a **source badge**:
|
### Integrations
|
||||||
- **BCS Official** – from Bahmcloud index
|
|
||||||
- **HACS** – from HACS official list (enable with the toggle)
|
|
||||||
- **Custom** – added by you
|
|
||||||
|
|
||||||
You can **filter by source** with the **Source** dropdown (All / BCS Official / HACS / Custom).
|
- 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 and uninstall through the store
|
||||||
|
- Intended for blueprint repositories without integration-specific folder structures
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## UI Overview
|
## UI Overview
|
||||||
|
|
||||||
Top bar:
|
Store view:
|
||||||
- **Search** (name/description)
|
|
||||||
- **Source** filter (All/BCS Official/HACS/Custom)
|
|
||||||
- **Category** filter (actually only Integrations are supported)
|
|
||||||
- **State-Filter** filter (All/Installed/Not installed/Update available/Custom repos)
|
|
||||||
- **Sort** (name (A-Z)/Updates first/Installed first)
|
|
||||||
- **HACS official** toggle (on/off)
|
|
||||||
|
|
||||||
Repository card:
|
- Search
|
||||||
- Name, description, badges (source, installed/update), category
|
- Source filter
|
||||||
- Buttons: **Install / Update / Uninstall**
|
- Category filter
|
||||||
- **Readme** expandable
|
- State filter
|
||||||
- **Open** to see details (available versions, metadata)
|
- 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 Integrations
|
## Finding Repositories
|
||||||
|
|
||||||
1. Use **Search** to filter by keywords.
|
1. Use **Search** to filter by name, description, or URL.
|
||||||
2. Combine with **Category** and **Source**.
|
2. Use **Source** to switch between BCS Official, HACS, and Custom repositories.
|
||||||
3. Sort to surface desired results.
|
3. Use **Category** to focus on a specific content type.
|
||||||
|
4. Use **Pinned** filtering or sorting to surface repositories you watch often.
|
||||||
|
|
||||||
Descriptions and latest versions are filled progressively by a background process; opening a repo loads details on demand.
|
Descriptions, metadata, and latest versions may appear progressively because BCS enriches repository information in the background.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
1. Open a repository.
|
### Integration Install
|
||||||
2. Optionally select **Install version** (default: **Latest**).
|
|
||||||
3. Click **Install** and wait for confirmation.
|
|
||||||
4. Follow the **Restart required** prompt. (or use restart toggle in HomeAssistant Settings)
|
|
||||||
|
|
||||||
**What happens internally**
|
1. Open a repository.
|
||||||
- BCS downloads the repository ZIP for the selected version (release/tag/branch).
|
2. Optionally select a version.
|
||||||
- It extracts all integrations found under `custom_components/<domain>` and deploys them.
|
3. Click **Install**.
|
||||||
- It saves the **installed version (ref)** to track updates reliably, even if the repo’s own `manifest.json` is wrong/outdated.
|
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 / Downgrading
|
## Selecting Versions
|
||||||
|
|
||||||
- Use the **Install version** dropdown in the detail view.
|
- Use the version selector in the detail view.
|
||||||
- Choose **Latest** or a previous **release/tag**.
|
- `Latest` uses the newest provider release or tag BCS can determine.
|
||||||
- Installing a chosen ref **pins** the integration to that ref (no surprise updates).
|
- You can install an older release or tag when available.
|
||||||
- You can upgrade again later by selecting **Latest** and clicking **Update**.
|
- Release notes are shown when the selected version is a provider release with a body.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
- The **Update** button appears when `latest_version` differs from your **installed version (ref)**.
|
### Integrations
|
||||||
- Updates are also available via **Home Assistant → Settings → Updates** (native Update entity).
|
|
||||||
- Clicking **Update** runs the same safe pipeline as **Install** (with backup).
|
|
||||||
|
|
||||||
**Tip:** Opening a repository detail view forces an immediate check for the latest version for that repo.
|
- 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 reinstalled from another selected version.
|
||||||
|
- The current blueprint path handling is focused on direct deployment to the blueprints folder.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Uninstalling
|
## Uninstalling
|
||||||
|
|
||||||
- Click **Uninstall** on the repository.
|
### Integrations
|
||||||
- BCS removes the integration folders under `custom_components/<domain>`.
|
|
||||||
- The installed state is cleared in the Store.
|
- Removes the installed integration directories from `/config/custom_components`
|
||||||
- Restart Home Assistant if prompted.
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Backups & Restore
|
## Pinned Repositories
|
||||||
|
|
||||||
Before an update/install over existing files, BCS creates a backup:
|
You can pin repositories to keep important items easy to find.
|
||||||
|
|
||||||
```
|
Current behavior:
|
||||||
/config/.bcs_backups/<domain>/<timestamp>/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Restore**:
|
- Pin or unpin from the detail view
|
||||||
1. Open the repository in store.
|
- Filter the store by pinned repositories
|
||||||
2. Select **Restore…**.
|
- Sort the store with pinned repositories first
|
||||||
3. Pick one of the **last backups** (up to retention limit). (Version details are showed in ())
|
- Pinned repositories show a dedicated badge in the store overview
|
||||||
4. Confirm – BCS restores files and reconciles installed version to the restored ref.
|
- Pinned state is persisted in settings
|
||||||
5. Restart Home Assistant if prompted.
|
|
||||||
|
|
||||||
If the old backup lacks metadata, BCS best‑effort derives the installed version from the backup’s `manifest.json`, or marks the ref as `restored:<timestamp>` so updates remain possible.
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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 currently intended for integrations.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Custom Repositories
|
## Custom Repositories
|
||||||
|
|
||||||
You can add any public repository (GitHub/GitLab/Gitea/Any other Git Hoster). BCS will attempt to detect:
|
You can add any public repository URL that matches a supported provider style and contains supported content.
|
||||||
- provider & default branch
|
|
||||||
- latest version (release/tag/atom)
|
|
||||||
- repo metadata (prefer `bcs.yaml`, fallback `hacs.json/hacs.yaml`)
|
|
||||||
- readme (common filenames)
|
|
||||||
|
|
||||||
**Add an custom repository**(typical flows):
|
BCS attempts to detect:
|
||||||
- From the Store UI
|
|
||||||
1. If you start up Bahmcloud Store, you are on tab "Store"
|
|
||||||
2. Go to tab "Manage"
|
|
||||||
3. Add Repository URL (Example: ``https://git.bahmcloud.de/bahmcloud/bahmcloud_store``) and (optional) Name you want to show up (will be grabbed from GIT Repository)
|
|
||||||
4. Repository show up in Custom Repositories-list and in "Store" Tab, if available
|
|
||||||
|
|
||||||
**Remove an custom repository**(typical flows):
|
- provider
|
||||||
- From the Store UI
|
- default branch
|
||||||
1. If you start up Bahmcloud Store, you are on tab "Store"
|
- latest version
|
||||||
2. Go to tab "Manage"
|
- metadata from `bcs.yaml`, `hacs.yaml`, or `hacs.json`
|
||||||
3. Your repository you want to remove (if its custom) show up in the list
|
- README content
|
||||||
4. Press "Remove" Button to delete from Store (Attention: If you Remove installed Repository, you won´t be able to Uninstall from Store, you have to remove by your own (Delete Folder from ```config/custom_components/*```))
|
|
||||||
|
|
||||||
Custom repos get the **Custom** badge and can be filtered via **Source**.
|
Custom repositories get a **Custom** badge and can be filtered separately.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## HACS Repositories
|
## HACS Repositories
|
||||||
|
|
||||||
Enable the **HACS official** toggle to include official HACS integrations.
|
Enable the HACS toggle to include official HACS integration repositories.
|
||||||
|
|
||||||
- BCS downloads the HACS integration list and maps **human‑readable names/descriptions** from HACS metadata.
|
Current scope:
|
||||||
- HACS entries are **not** part of your BCS-Official Repositories (avoid duplicate entries).
|
|
||||||
|
|
||||||
With many HACS repos, metadata loads in the background; names/descriptions appear progressively and are cached.
|
- HACS source support is currently focused on integrations
|
||||||
|
- HACS metadata is used to improve naming and descriptions
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Update Entities in Home Assistant
|
## Update Entities
|
||||||
|
|
||||||
BCS exposes update entities for installed repos:
|
BCS exposes Home Assistant update entities for installed integrations.
|
||||||
- Found under **Settings → Updates**
|
|
||||||
- Clicking **Install** triggers BCS update pipeline
|
- Found under **Settings -> Updates**
|
||||||
- Shows **installed** and **latest** versions (BCS ref logic)
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Background Caching & Performance
|
## Performance and Caching
|
||||||
|
|
||||||
- **Fast initial list**: index + local cache only
|
BCS is designed to keep startup responsive.
|
||||||
- **Background enrichment**: provider info, latest version, metadata, description, readme (best effort)
|
|
||||||
- **On‑demand**: opening a repo triggers immediate enrichment; data is **persisted** to cache
|
- Heavy work runs after Home Assistant startup
|
||||||
- **Persistent cache**: survives HA restarts; speeds up subsequent runs
|
- Repo data is enriched in the background
|
||||||
- **Refresh**: immediately rechecks installed repos and key metadata
|
- 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 Required
|
## Restart Behavior
|
||||||
|
|
||||||
After install, update, or restore, BCS raises a **Restart required** item in Home Assistant (Repairs). You can restart directly from there.
|
Restart usually matters only for integration changes.
|
||||||
|
|
||||||
|
- Integration install/update/uninstall/restore: restart expected
|
||||||
|
- Blueprint install/uninstall: restart usually not needed
|
||||||
|
|
||||||
|
BCS uses a Home Assistant repair flow to surface restart requirements for integration changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **New release but no update**: Open the repo detail once; ensure it’s a **release/tag** (commits alone don’t change the ref).
|
- **Latest version missing**: open the repository detail once to force a recheck
|
||||||
- **Descriptions/Latest missing**: Wait for background enrichment or open the repo detail (forces enrichment). Cached afterwards.
|
- **Release notes missing**: the selected version may be a tag or branch without a provider release body
|
||||||
(You also can Install new version by selecting in Version tab)
|
- **Blueprint install fails**: verify the repository really contains a `blueprints/` folder
|
||||||
- **Slow startup**: BCS schedules heavy work after HA started. Keep indexes reasonable.
|
- **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
|
## FAQ
|
||||||
|
|
||||||
- **Backups path?** `/config/.bcs_backups/<domain>/<timestamp>/`
|
### Where are integrations installed?
|
||||||
- **Install path?** `/config/custom_components/<domain>`
|
|
||||||
- **Downgrade?** Yes, pick an older version and install.
|
`/config/custom_components/<domain>`
|
||||||
- **Restart needed?** Yes, after install/update/restore.
|
|
||||||
|
### 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.
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
frontend_url_path="bahmcloud-store",
|
frontend_url_path="bahmcloud-store",
|
||||||
webcomponent_name="bahmcloud-store-panel",
|
webcomponent_name="bahmcloud-store-panel",
|
||||||
# IMPORTANT: bump v to avoid caching old JS
|
# IMPORTANT: bump v to avoid caching old JS
|
||||||
module_url="/api/bahmcloud_store_static/panel.js?v=111",
|
module_url="/api/bahmcloud_store_static/panel.js?v=112",
|
||||||
sidebar_title="Bahmcloud Store",
|
sidebar_title="Bahmcloud Store",
|
||||||
sidebar_icon="mdi:store",
|
sidebar_icon="mdi:store",
|
||||||
require_admin=True,
|
require_admin=True,
|
||||||
|
|||||||
@@ -1172,13 +1172,17 @@ class BCSCore:
|
|||||||
"latest_version": r.latest_version,
|
"latest_version": r.latest_version,
|
||||||
"latest_version_source": r.latest_version_source,
|
"latest_version_source": r.latest_version_source,
|
||||||
"category": r.meta_category,
|
"category": r.meta_category,
|
||||||
|
"category_key": self._repo_install_type(r),
|
||||||
|
"install_target": self._repo_install_target(r),
|
||||||
"meta_author": r.meta_author,
|
"meta_author": r.meta_author,
|
||||||
"meta_maintainer": r.meta_maintainer,
|
"meta_maintainer": r.meta_maintainer,
|
||||||
"meta_source": r.meta_source,
|
"meta_source": r.meta_source,
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
|
"install_type": str(inst.get("install_type") if isinstance(inst, dict) else self._repo_install_type(r)),
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
"installed_manifest_version": installed_manifest_version,
|
"installed_manifest_version": installed_manifest_version,
|
||||||
"installed_domains": installed_domains,
|
"installed_domains": installed_domains,
|
||||||
|
"installed_paths": list(inst.get("installed_paths") or []) if isinstance(inst, dict) else [],
|
||||||
"favorite": self.is_favorite_repo(r.id),
|
"favorite": self.is_favorite_repo(r.id),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1220,6 +1224,32 @@ class BCSCore:
|
|||||||
return str(repo.default_branch).strip()
|
return str(repo.default_branch).strip()
|
||||||
return "main"
|
return "main"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_category_key(category: str | None) -> str:
|
||||||
|
raw = str(category or "").strip().lower()
|
||||||
|
if raw in ("integration", "integrations"):
|
||||||
|
return "integration"
|
||||||
|
if raw in ("blueprint", "blueprints"):
|
||||||
|
return "blueprint"
|
||||||
|
if raw in ("template", "templates"):
|
||||||
|
return "template"
|
||||||
|
if raw in ("lovelace", "dashboard", "dashboards", "lovelace design", "lovelace designs"):
|
||||||
|
return "lovelace"
|
||||||
|
return "integration"
|
||||||
|
|
||||||
|
def _repo_install_type(self, repo: RepoItem | None) -> str:
|
||||||
|
return self._normalize_category_key(getattr(repo, "meta_category", None))
|
||||||
|
|
||||||
|
def _repo_install_target(self, repo: RepoItem | None) -> str:
|
||||||
|
install_type = self._repo_install_type(repo)
|
||||||
|
if install_type == "blueprint":
|
||||||
|
return "/config/blueprints"
|
||||||
|
if install_type == "template":
|
||||||
|
return "/config"
|
||||||
|
if install_type == "lovelace":
|
||||||
|
return "/config"
|
||||||
|
return "/config/custom_components"
|
||||||
|
|
||||||
def _build_zip_url(self, repo_url: str, ref: str) -> str:
|
def _build_zip_url(self, repo_url: str, ref: str) -> str:
|
||||||
ref = (ref or "").strip()
|
ref = (ref or "").strip()
|
||||||
if not ref:
|
if not ref:
|
||||||
@@ -1286,6 +1316,18 @@ class BCSCore:
|
|||||||
return candidate
|
return candidate
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_blueprints_root(extract_root: Path) -> Path | None:
|
||||||
|
direct = extract_root / "blueprints"
|
||||||
|
if direct.exists() and direct.is_dir():
|
||||||
|
return direct
|
||||||
|
|
||||||
|
for child in extract_root.iterdir():
|
||||||
|
candidate = child / "blueprints"
|
||||||
|
if candidate.exists() and candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
async def _ensure_backup_root(self) -> None:
|
async def _ensure_backup_root(self) -> None:
|
||||||
"""Create backup root directory if needed."""
|
"""Create backup root directory if needed."""
|
||||||
def _mkdir() -> None:
|
def _mkdir() -> None:
|
||||||
@@ -1640,15 +1682,25 @@ class BCSCore:
|
|||||||
|
|
||||||
for it in items:
|
for it in items:
|
||||||
domains = [str(d) for d in (it.domains or []) if str(d).strip()]
|
domains = [str(d) for d in (it.domains or []) if str(d).strip()]
|
||||||
|
install_type = str(getattr(it, "install_type", "integration") or "integration").strip() or "integration"
|
||||||
|
installed_paths = [str(p) for p in (getattr(it, "installed_paths", None) or []) if str(p).strip()]
|
||||||
|
|
||||||
# A repo is considered "present" if at least one of its domains
|
# A repo is considered "present" if at least one of its domains
|
||||||
# exists and contains a manifest.json.
|
# exists and contains a manifest.json.
|
||||||
present = False
|
present = False
|
||||||
|
if install_type == "integration":
|
||||||
for d in domains:
|
for d in domains:
|
||||||
p = cc_root / d
|
p = cc_root / d
|
||||||
if p.is_dir() and (p / "manifest.json").exists():
|
if p.is_dir() and (p / "manifest.json").exists():
|
||||||
present = True
|
present = True
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
cfg_root = Path(self.hass.config.path(""))
|
||||||
|
for rel in installed_paths:
|
||||||
|
p = cfg_root / rel
|
||||||
|
if p.exists():
|
||||||
|
present = True
|
||||||
|
break
|
||||||
|
|
||||||
if not present:
|
if not present:
|
||||||
to_remove.append(it.repo_id)
|
to_remove.append(it.repo_id)
|
||||||
@@ -1657,6 +1709,8 @@ class BCSCore:
|
|||||||
cache[it.repo_id] = {
|
cache[it.repo_id] = {
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"domains": domains,
|
"domains": domains,
|
||||||
|
"install_type": install_type,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
"installed_version": it.installed_version,
|
"installed_version": it.installed_version,
|
||||||
"installed_manifest_version": it.installed_manifest_version,
|
"installed_manifest_version": it.installed_manifest_version,
|
||||||
"ref": it.ref,
|
"ref": it.ref,
|
||||||
@@ -1692,6 +1746,24 @@ class BCSCore:
|
|||||||
if path.exists() and path.is_dir():
|
if path.exists() and path.is_dir():
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
|
||||||
|
def _remove_file(path: Path) -> None:
|
||||||
|
if path.exists() and path.is_file():
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
def _prune_empty_parents(path: Path, stop_at: Path) -> None:
|
||||||
|
cur = path.parent
|
||||||
|
while cur != stop_at and str(cur).startswith(str(stop_at)):
|
||||||
|
try:
|
||||||
|
if any(cur.iterdir()):
|
||||||
|
break
|
||||||
|
cur.rmdir()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
cur = cur.parent
|
||||||
|
|
||||||
|
install_type = str(getattr(inst, "install_type", "integration") or "integration").strip() or "integration"
|
||||||
|
|
||||||
|
if install_type == "integration":
|
||||||
for domain in inst.domains:
|
for domain in inst.domains:
|
||||||
d = str(domain).strip()
|
d = str(domain).strip()
|
||||||
if not d:
|
if not d:
|
||||||
@@ -1699,17 +1771,25 @@ class BCSCore:
|
|||||||
target = cc_root / d
|
target = cc_root / d
|
||||||
await self.hass.async_add_executor_job(_remove_dir, target)
|
await self.hass.async_add_executor_job(_remove_dir, target)
|
||||||
removed.append(d)
|
removed.append(d)
|
||||||
|
elif install_type == "blueprint":
|
||||||
|
cfg_root = Path(self.hass.config.path(""))
|
||||||
|
blueprints_root = Path(self.hass.config.path("blueprints"))
|
||||||
|
for rel in [str(p).strip() for p in (inst.installed_paths or []) if str(p).strip()]:
|
||||||
|
target = cfg_root / rel
|
||||||
|
await self.hass.async_add_executor_job(_remove_file, target)
|
||||||
|
await self.hass.async_add_executor_job(_prune_empty_parents, target, blueprints_root)
|
||||||
|
removed.append(rel)
|
||||||
|
|
||||||
await self.storage.remove_installed_repo(repo_id)
|
await self.storage.remove_installed_repo(repo_id)
|
||||||
await self._refresh_installed_cache()
|
await self._refresh_installed_cache()
|
||||||
|
|
||||||
# Show restart required in Settings.
|
# Show restart required in Settings.
|
||||||
if removed:
|
if removed and install_type == "integration":
|
||||||
self._mark_restart_required()
|
self._mark_restart_required()
|
||||||
|
|
||||||
_LOGGER.info("BCS uninstall complete: repo_id=%s removed_domains=%s", repo_id, removed)
|
_LOGGER.info("BCS uninstall complete: repo_id=%s removed_domains=%s", repo_id, removed)
|
||||||
self.signal_updated()
|
self.signal_updated()
|
||||||
return {"ok": True, "repo_id": repo_id, "removed": removed, "restart_required": bool(removed)}
|
return {"ok": True, "repo_id": repo_id, "removed": removed, "restart_required": bool(removed) if install_type == "integration" else False}
|
||||||
|
|
||||||
async def install_repo(self, repo_id: str, *, version: str | None = None) -> dict[str, Any]:
|
async def install_repo(self, repo_id: str, *, version: str | None = None) -> dict[str, Any]:
|
||||||
repo = self.get_repo(repo_id)
|
repo = self.get_repo(repo_id)
|
||||||
@@ -1724,7 +1804,9 @@ class BCSCore:
|
|||||||
_LOGGER.info("BCS install started: repo_id=%s ref=%s zip_url=%s", repo_id, ref, zip_url)
|
_LOGGER.info("BCS install started: repo_id=%s ref=%s zip_url=%s", repo_id, ref, zip_url)
|
||||||
|
|
||||||
installed_domains: list[str] = []
|
installed_domains: list[str] = []
|
||||||
|
installed_paths: list[str] = []
|
||||||
backups: dict[str, Path] = {}
|
backups: dict[str, Path] = {}
|
||||||
|
install_type = self._repo_install_type(repo)
|
||||||
|
|
||||||
inst_before = self.get_installed(repo_id) or {}
|
inst_before = self.get_installed(repo_id) or {}
|
||||||
backup_meta = {
|
backup_meta = {
|
||||||
@@ -1745,6 +1827,31 @@ class BCSCore:
|
|||||||
await self._download_zip(zip_url, zip_path)
|
await self._download_zip(zip_url, zip_path)
|
||||||
await self._extract_zip(zip_path, extract_dir)
|
await self._extract_zip(zip_path, extract_dir)
|
||||||
|
|
||||||
|
if install_type == "blueprint":
|
||||||
|
blueprints_root = self._find_blueprints_root(extract_dir)
|
||||||
|
if not blueprints_root:
|
||||||
|
raise BCSInstallError("blueprints folder not found in repository ZIP")
|
||||||
|
|
||||||
|
config_root = Path(self.hass.config.path(""))
|
||||||
|
target_root = Path(self.hass.config.path("blueprints"))
|
||||||
|
|
||||||
|
def _copy_blueprints() -> list[str]:
|
||||||
|
copied: list[str] = []
|
||||||
|
target_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
for src in blueprints_root.rglob("*"):
|
||||||
|
if not src.is_file():
|
||||||
|
continue
|
||||||
|
rel = src.relative_to(blueprints_root)
|
||||||
|
dest = target_root / rel
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
copied.append(str(Path("blueprints") / rel).replace("\\", "/"))
|
||||||
|
return copied
|
||||||
|
|
||||||
|
installed_paths = await self.hass.async_add_executor_job(_copy_blueprints)
|
||||||
|
if not installed_paths:
|
||||||
|
raise BCSInstallError("No blueprint files found under blueprints/")
|
||||||
|
else:
|
||||||
cc_root = self._find_custom_components_root(extract_dir)
|
cc_root = self._find_custom_components_root(extract_dir)
|
||||||
if not cc_root:
|
if not cc_root:
|
||||||
raise BCSInstallError("custom_components folder not found in repository ZIP")
|
raise BCSInstallError("custom_components folder not found in repository ZIP")
|
||||||
@@ -1777,25 +1884,28 @@ class BCSCore:
|
|||||||
if not installed_domains:
|
if not installed_domains:
|
||||||
raise BCSInstallError("No integrations found under custom_components/ (missing manifest.json)")
|
raise BCSInstallError("No integrations found under custom_components/ (missing manifest.json)")
|
||||||
|
|
||||||
installed_manifest_version = await self._read_installed_manifest_version(installed_domains[0])
|
installed_manifest_version = await self._read_installed_manifest_version(installed_domains[0]) if installed_domains else None
|
||||||
installed_version = ref
|
installed_version = ref
|
||||||
|
|
||||||
await self.storage.set_installed_repo(
|
await self.storage.set_installed_repo(
|
||||||
repo_id=repo_id,
|
repo_id=repo_id,
|
||||||
url=repo.url,
|
url=repo.url,
|
||||||
domains=installed_domains,
|
domains=installed_domains,
|
||||||
|
install_type=install_type,
|
||||||
|
installed_paths=installed_paths,
|
||||||
installed_version=installed_version,
|
installed_version=installed_version,
|
||||||
installed_manifest_version=installed_manifest_version,
|
installed_manifest_version=installed_manifest_version,
|
||||||
ref=ref,
|
ref=ref,
|
||||||
)
|
)
|
||||||
await self._refresh_installed_cache()
|
await self._refresh_installed_cache()
|
||||||
|
|
||||||
|
if install_type == "integration":
|
||||||
self._mark_restart_required()
|
self._mark_restart_required()
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"BCS install complete: repo_id=%s domains=%s installed_ref=%s manifest_version=%s",
|
"BCS install complete: repo_id=%s domains=%s installed_ref=%s manifest_version=%s",
|
||||||
repo_id,
|
repo_id,
|
||||||
installed_domains,
|
installed_domains if installed_domains else installed_paths,
|
||||||
installed_version,
|
installed_version,
|
||||||
installed_manifest_version,
|
installed_manifest_version,
|
||||||
)
|
)
|
||||||
@@ -1804,9 +1914,11 @@ class BCSCore:
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"repo_id": repo_id,
|
"repo_id": repo_id,
|
||||||
"domains": installed_domains,
|
"domains": installed_domains,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
|
"install_type": install_type,
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
"installed_manifest_version": installed_manifest_version,
|
"installed_manifest_version": installed_manifest_version,
|
||||||
"restart_required": True,
|
"restart_required": True if install_type == "integration" else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "bahmcloud_store",
|
"domain": "bahmcloud_store",
|
||||||
"name": "Bahmcloud Store",
|
"name": "Bahmcloud Store",
|
||||||
"version": "0.7.4",
|
"version": "0.7.5",
|
||||||
"documentation": "https://git.bahmcloud.de/bahmcloud/bahmcloud_store",
|
"documentation": "https://git.bahmcloud.de/bahmcloud/bahmcloud_store",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"platforms": ["update"],
|
"platforms": ["update"],
|
||||||
|
|||||||
@@ -1270,6 +1270,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: -",
|
this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: -",
|
||||||
this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
|
this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
|
||||||
this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
|
this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
|
||||||
|
this._safeText(r?.install_target) ? `Target: ${this._safeText(r?.install_target)}` : null,
|
||||||
this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
|
this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
|
||||||
this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
|
this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
|
||||||
this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
|
this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
|
||||||
@@ -1312,8 +1313,10 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
const installed = this._asBoolStrict(r?.installed);
|
const installed = this._asBoolStrict(r?.installed);
|
||||||
const installedVersion = this._safeText(r?.installed_version);
|
const installedVersion = this._safeText(r?.installed_version);
|
||||||
const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : [];
|
const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : [];
|
||||||
|
const installedPaths = Array.isArray(r?.installed_paths) ? r.installed_paths : [];
|
||||||
const latestVersion = this._safeText(r?.latest_version);
|
const latestVersion = this._safeText(r?.latest_version);
|
||||||
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(repoId);
|
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(repoId);
|
||||||
|
const installType = this._safeText(r?.install_type) || "integration";
|
||||||
|
|
||||||
const busyInstall = this._installingRepoId === repoId;
|
const busyInstall = this._installingRepoId === repoId;
|
||||||
const busyUpdate = this._updatingRepoId === repoId;
|
const busyUpdate = this._updatingRepoId === repoId;
|
||||||
@@ -1376,7 +1379,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
const installBtn = `<button class="primary" id="btnInstall" ${installed || busy ? "disabled" : ""}>${busyInstall ? "Installing…" : installed ? "Installed" : "Install"}</button>`;
|
const installBtn = `<button class="primary" id="btnInstall" ${installed || busy ? "disabled" : ""}>${busyInstall ? "Installing…" : installed ? "Installed" : "Install"}</button>`;
|
||||||
const updateBtn = `<button class="primary" id="btnUpdate" ${!updateAvailable || busy ? "disabled" : ""}>${busyUpdate ? "Updating…" : updateAvailable ? "Update" : "Up to date"}</button>`;
|
const updateBtn = `<button class="primary" id="btnUpdate" ${!updateAvailable || busy ? "disabled" : ""}>${busyUpdate ? "Updating…" : updateAvailable ? "Update" : "Up to date"}</button>`;
|
||||||
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
|
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
|
||||||
const restoreBtn = `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`;
|
const restoreBtn = installType === "integration"
|
||||||
|
? `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`
|
||||||
|
: ``;
|
||||||
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
||||||
|
|
||||||
const restartHint = this._restartRequired
|
const restartHint = this._restartRequired
|
||||||
@@ -1424,6 +1429,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
<div><strong>Installed version:</strong> ${this._esc(installedVersion || "-")}</div>
|
<div><strong>Installed version:</strong> ${this._esc(installedVersion || "-")}</div>
|
||||||
<div><strong>Latest version:</strong> ${this._esc(latestVersion || "-")}</div>
|
<div><strong>Latest version:</strong> ${this._esc(latestVersion || "-")}</div>
|
||||||
<div style="margin-top:6px;"><strong>Domains:</strong> ${installedDomains.length ? this._esc(installedDomains.join(", ")) : "-"}</div>
|
<div style="margin-top:6px;"><strong>Domains:</strong> ${installedDomains.length ? this._esc(installedDomains.join(", ")) : "-"}</div>
|
||||||
|
<div style="margin-top:6px;"><strong>Installed paths:</strong> ${installedPaths.length ? this._esc(installedPaths.join(", ")) : "-"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${versionSelect}
|
${versionSelect}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class InstalledRepo:
|
|||||||
url: str
|
url: str
|
||||||
domains: list[str]
|
domains: list[str]
|
||||||
installed_at: int
|
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_version: str | None = None # BCS "installed ref" (tag/release/branch)
|
||||||
installed_manifest_version: str | None = None # informational only
|
installed_manifest_version: str | None = None # informational only
|
||||||
ref: str | None = None # kept for backward compatibility / diagnostics
|
ref: str | None = None # kept for backward compatibility / diagnostics
|
||||||
@@ -197,6 +199,11 @@ class BCSStorage:
|
|||||||
if not isinstance(domains, list):
|
if not isinstance(domains, list):
|
||||||
domains = []
|
domains = []
|
||||||
domains = [str(d) for d in domains if str(d).strip()]
|
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")
|
installed_version = entry.get("installed_version")
|
||||||
ref = entry.get("ref")
|
ref = entry.get("ref")
|
||||||
@@ -213,6 +220,8 @@ class BCSStorage:
|
|||||||
url=str(entry.get("url") or ""),
|
url=str(entry.get("url") or ""),
|
||||||
domains=domains,
|
domains=domains,
|
||||||
installed_at=int(entry.get("installed_at") or 0),
|
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_version=str(installed_version) if installed_version else None,
|
||||||
installed_manifest_version=str(installed_manifest_version) if installed_manifest_version else None,
|
installed_manifest_version=str(installed_manifest_version) if installed_manifest_version else None,
|
||||||
ref=str(ref) if ref else None,
|
ref=str(ref) if ref else None,
|
||||||
@@ -238,9 +247,11 @@ class BCSStorage:
|
|||||||
repo_id: str,
|
repo_id: str,
|
||||||
url: str,
|
url: str,
|
||||||
domains: list[str],
|
domains: list[str],
|
||||||
installed_version: str | None,
|
installed_version: str | None = None,
|
||||||
installed_manifest_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:
|
) -> None:
|
||||||
data = await self._load()
|
data = await self._load()
|
||||||
installed = data.get("installed_repos", {})
|
installed = data.get("installed_repos", {})
|
||||||
@@ -252,6 +263,8 @@ class BCSStorage:
|
|||||||
"repo_id": str(repo_id),
|
"repo_id": str(repo_id),
|
||||||
"url": str(url),
|
"url": str(url),
|
||||||
"domains": [str(d) for d in (domains or []) if str(d).strip()],
|
"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()),
|
"installed_at": int(time.time()),
|
||||||
# IMPORTANT: this is what BCS uses as "installed version" (ref/tag/branch)
|
# IMPORTANT: this is what BCS uses as "installed version" (ref/tag/branch)
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
|
|||||||
@@ -540,6 +540,9 @@ class BCSRepoDetailView(HomeAssistantView):
|
|||||||
domains = inst.get("domains") or []
|
domains = inst.get("domains") or []
|
||||||
if not isinstance(domains, list):
|
if not isinstance(domains, list):
|
||||||
domains = []
|
domains = []
|
||||||
|
installed_paths = inst.get("installed_paths") or []
|
||||||
|
if not isinstance(installed_paths, list):
|
||||||
|
installed_paths = []
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -556,13 +559,17 @@ class BCSRepoDetailView(HomeAssistantView):
|
|||||||
"latest_version": repo.latest_version,
|
"latest_version": repo.latest_version,
|
||||||
"latest_version_source": repo.latest_version_source,
|
"latest_version_source": repo.latest_version_source,
|
||||||
"category": repo.meta_category,
|
"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_author": repo.meta_author,
|
||||||
"meta_maintainer": repo.meta_maintainer,
|
"meta_maintainer": repo.meta_maintainer,
|
||||||
"meta_source": repo.meta_source,
|
"meta_source": repo.meta_source,
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
|
"install_type": inst.get("install_type") or self.core._repo_install_type(repo),
|
||||||
"installed_version": inst.get("installed_version"),
|
"installed_version": inst.get("installed_version"),
|
||||||
"installed_manifest_version": inst.get("installed_manifest_version"),
|
"installed_manifest_version": inst.get("installed_manifest_version"),
|
||||||
"installed_domains": domains,
|
"installed_domains": domains,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
"favorite": self.core.is_favorite_repo(repo.id),
|
"favorite": self.core.is_favorite_repo(repo.id),
|
||||||
}
|
}
|
||||||
}, status=200)
|
}, status=200)
|
||||||
|
|||||||
Reference in New Issue
Block a user