Upload 4 files
Browse files- Dockerfile +21 -0
- README.md +37 -0
- scripts/backup-homeassistant.py +79 -0
- scripts/start-homeassistant +46 -0
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM ghcr.io/home-assistant/home-assistant:stable
|
| 2 |
+
|
| 3 |
+
USER root
|
| 4 |
+
|
| 5 |
+
RUN apk add --no-cache git bash \
|
| 6 |
+
&& git clone --depth 1 https://github.com/al-one/hass-xiaomi-miot.git /tmp/hass-xiaomi-miot \
|
| 7 |
+
&& mkdir -p /opt/ha-custom-components \
|
| 8 |
+
&& cp -a /tmp/hass-xiaomi-miot/custom_components/xiaomi_miot /opt/hass-custom-components/xiaomi_miot \
|
| 9 |
+
&& rm -rf /tmp/hass-xiaomi-miot \
|
| 10 |
+
&& python3 -m pip install --no-cache-dir --disable-pip-version-check huggingface_hub \
|
| 11 |
+
&& mkdir -p /data/home-assistant /config
|
| 12 |
+
|
| 13 |
+
COPY scripts/start-homeassistant /usr/local/bin/start-homeassistant
|
| 14 |
+
COPY scripts/backup-homeassistant.py /usr/local/bin/backup-homeassistant.py
|
| 15 |
+
RUN chmod +x /usr/local/bin/start-homeassistant
|
| 16 |
+
|
| 17 |
+
ENV HOME_ASSISTANT_CONFIG=/data/home-assistant
|
| 18 |
+
ENV PORT=7860
|
| 19 |
+
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
ENTRYPOINT ["/usr/local/bin/start-homeassistant"]
|
README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Home Assistant Mi Home
|
| 3 |
+
emoji: 🏠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
suggested_hardware: cpu-basic
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Home Assistant on Hugging Face Spaces
|
| 12 |
+
|
| 13 |
+
This Space runs Home Assistant with the Xiaomi Miot custom integration pre-installed.
|
| 14 |
+
|
| 15 |
+
## Required Space secrets
|
| 16 |
+
|
| 17 |
+
The web interface works without backup secrets, but the free Space disk is ephemeral.
|
| 18 |
+
For configuration recovery after a Space rebuild, create a private Dataset on the
|
| 19 |
+
Hugging Face Hub and add these Space secrets:
|
| 20 |
+
|
| 21 |
+
- `HF_TOKEN`: a token with write access to the private Dataset
|
| 22 |
+
- `BACKUP_REPO_ID`: Dataset id, for example `your-name/home-assistant-backup`
|
| 23 |
+
|
| 24 |
+
The startup script restores `homeassistant-latest.tar.gz` when it exists and uploads a
|
| 25 |
+
new snapshot every 10 minutes. The backup includes Home Assistant's `.storage` folder,
|
| 26 |
+
so integrations and onboarding state are retained. It excludes the recorder database,
|
| 27 |
+
logs, cache, and media to keep the backup small.
|
| 28 |
+
|
| 29 |
+
## First use
|
| 30 |
+
|
| 31 |
+
Open the Space at its public URL and complete the Home Assistant onboarding flow. In
|
| 32 |
+
**Settings -> Devices & services**, add **Xiaomi Miot Auto** and sign in with the Mi
|
| 33 |
+
Home account. This integration generally requires the Xiaomi cloud region that owns
|
| 34 |
+
the devices; it does not require the Home Assistant container to be on the same LAN.
|
| 35 |
+
|
| 36 |
+
Do not put `HF_TOKEN` in this repository or in `README.md`; use Space Settings ->
|
| 37 |
+
Repository secrets.
|
scripts/backup-homeassistant.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Restore and upload a compact Home Assistant configuration snapshot."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import tarfile
|
| 7 |
+
import tempfile
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
ARCHIVE_NAME = "homeassistant-latest.tar.gz"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def hub_api():
|
| 14 |
+
from huggingface_hub import HfApi
|
| 15 |
+
token = os.environ.get("HF_TOKEN")
|
| 16 |
+
repo_id = os.environ.get("BACKUP_REPO_ID")
|
| 17 |
+
if not token or not repo_id:
|
| 18 |
+
raise RuntimeError("HF_TOKEN and BACKUP_REPO_ID are required for Hub backups")
|
| 19 |
+
api = HfApi(token=token)
|
| 20 |
+
repo_type = os.environ.get("BACKUP_REPO_TYPE", "dataset")
|
| 21 |
+
try:
|
| 22 |
+
api.repo_info(repo_id=repo_id, repo_type=repo_type)
|
| 23 |
+
except Exception:
|
| 24 |
+
api.create_repo(repo_id=repo_id, repo_type=repo_type, private=True, exist_ok=True)
|
| 25 |
+
return api, repo_id, repo_type, token
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def archive_config(config: Path, archive: Path) -> None:
|
| 29 |
+
with tarfile.open(archive, "w:gz") as tar:
|
| 30 |
+
for path in config.rglob("*"):
|
| 31 |
+
if not path.is_file() or path.is_symlink(): continue
|
| 32 |
+
relative = path.relative_to(config).as_posix()
|
| 33 |
+
if relative in {"home-assistant_v2.db", "home-assistant.log"}: continue
|
| 34 |
+
if relative.endswith((".db-wal", ".db-shm")): continue
|
| 35 |
+
if relative.startswith((".cloud/", "tts/")): continue
|
| 36 |
+
tar.add(path, arcname=relative, recursive=False)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def backup(config: Path) -> None:
|
| 40 |
+
api, repo_id, repo_type, token = hub_api()
|
| 41 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 42 |
+
archive = Path(temp_dir) / ARCHIVE_NAME
|
| 43 |
+
archive_config(config, archive)
|
| 44 |
+
api.upload_file(path_or_fileobj=str(archive), path_in_repo=ARCHIVE_NAME,
|
| 45 |
+
repo_id=repo_id, repo_type=repo_type, token=token,
|
| 46 |
+
commit_message="Update Home Assistant configuration backup")
|
| 47 |
+
print(f"Uploaded {ARCHIVE_NAME} to {repo_type}/{repo_id}", flush=True)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def restore(config: Path) -> None:
|
| 51 |
+
try:
|
| 52 |
+
_, repo_id, repo_type, token = hub_api()
|
| 53 |
+
from huggingface_hub import hf_hub_download
|
| 54 |
+
archive = hf_hub_download(repo_id=repo_id, filename=ARCHIVE_NAME,
|
| 55 |
+
repo_type=repo_type, token=token, force_download=True)
|
| 56 |
+
except Exception as exc:
|
| 57 |
+
print(f"No Hub backup restored: {exc}", file=sys.stderr, flush=True)
|
| 58 |
+
return
|
| 59 |
+
config.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
with tarfile.open(archive, "r:gz") as tar:
|
| 61 |
+
root = config.resolve()
|
| 62 |
+
for member in tar.getmembers():
|
| 63 |
+
target = (config / member.name).resolve()
|
| 64 |
+
if root not in target.parents: raise RuntimeError(f"Unsafe backup path: {member.name}")
|
| 65 |
+
tar.extractall(config)
|
| 66 |
+
print(f"Restored {ARCHIVE_NAME} into {config}", flush=True)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def main() -> int:
|
| 70 |
+
parser = argparse.ArgumentParser()
|
| 71 |
+
parser.add_argument("action", choices=("backup", "restore"))
|
| 72 |
+
parser.add_argument("--config", required=True, type=Path)
|
| 73 |
+
args = parser.parse_args()
|
| 74 |
+
if not os.environ.get("HF_TOKEN") or not os.environ.get("BACKUP_REPO_ID"): return 0
|
| 75 |
+
(backup if args.action == "backup" else restore)(args.config)
|
| 76 |
+
return 0
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
if __name__ == "__main__": raise SystemExit(main())
|
scripts/start-homeassistant
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -Eeuo pipefail
|
| 3 |
+
|
| 4 |
+
CONFIG_DIR="${HOME_ASSISTANT_CONFIG:-/data/home-assistant}"
|
| 5 |
+
CUSTOM_COMPONENT="/opt/hass-custom-components/xiaomi_miot"
|
| 6 |
+
|
| 7 |
+
mkdir -p "$CONFIG_DIR/custom_components"
|
| 8 |
+
if [[ ! -f "$CONFIG_DIR/configuration.yaml" ]]; then
|
| 9 |
+
python3 /usr/local/bin/backup-homeassistant.py restore --config "$CONFIG_DIR" || true
|
| 10 |
+
fi
|
| 11 |
+
|
| 12 |
+
if [[ ! -d "$CONFIG_DIR/custom_components/xiaomi_miot" ]]; then
|
| 13 |
+
cp -a "$CUSTOM_COMPONENT" "$CONFIG_DIR/custom_components/xiaomi_miot"
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
if [[ ! -f "$CONFIG_DIR/configuration.yaml" ]]; then
|
| 17 |
+
cat > "$CONFIG_DIR/configuration.yaml" <<'YAML'
|
| 18 |
+
default_config:
|
| 19 |
+
|
| 20 |
+
http:
|
| 21 |
+
server_port: 7860
|
| 22 |
+
YAML
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
if [[ -n "${HF_TOKEN:-}" && -n "${BACKUP_REPO_ID:-}" ]]; then
|
| 26 |
+
(
|
| 27 |
+
while sleep 600; do
|
| 28 |
+
python3 /usr/local/bin/backup-homeassistant.py backup --config "$CONFIG_DIR" || true
|
| 29 |
+
done
|
| 30 |
+
) &
|
| 31 |
+
BACKUP_PID=$!
|
| 32 |
+
else
|
| 33 |
+
BACKUP_PID=""
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
cleanup() {
|
| 37 |
+
if [[ -n "$BACKUP_PID" ]]; then kill "$BACKUP_PID" 2>/dev/null || true; fi
|
| 38 |
+
if [[ -n "${HF_TOKEN:-}" && -n "${BACKUP_REPO_ID:-}" ]]; then
|
| 39 |
+
python3 /usr/local/bin/backup-homeassistant.py backup --config "$CONFIG_DIR" || true
|
| 40 |
+
fi
|
| 41 |
+
}
|
| 42 |
+
trap cleanup EXIT TERM INT
|
| 43 |
+
|
| 44 |
+
hass --config "$CONFIG_DIR" --port 7860 &
|
| 45 |
+
HA_PID=$!
|
| 46 |
+
wait "$HA_PID"
|