YYP0502 commited on
Commit
ad2cbeb
·
verified ·
1 Parent(s): 441269b

Upload 10 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .venv
2
+ **/__pycache__
3
+ .pytest_cache
4
+ .git
5
+ tests
6
+ .env
7
+ *.zip
Dockerfile CHANGED
@@ -2,7 +2,7 @@ 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/ha-custom-components/xiaomi_miot \
@@ -10,6 +10,19 @@ RUN apk add --no-cache git bash \
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
 
2
 
3
  USER root
4
 
5
+ RUN apk add --no-cache git bash nginx \
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/ha-custom-components/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
+ # Pre-install the China-region Haier Smart Home custom integration.
14
+ RUN git clone --depth 1 https://github.com/sfairy/ha_haier.git /tmp/ha_haier \
15
+ && mkdir -p /opt/ha-custom-components \
16
+ && cp -a /tmp/ha_haier/custom_components/haier /opt/ha-custom-components/haier \
17
+ && rm -rf /tmp/ha_haier
18
+
19
+ COPY mcp_server/requirements.txt /opt/mcp-server/requirements.txt
20
+ RUN python3 -m venv /opt/mcp-venv \
21
+ && /opt/mcp-venv/bin/pip install --no-cache-dir -r /opt/mcp-server/requirements.txt
22
+ COPY mcp_server/server.py /opt/mcp-server/server.py
23
+ COPY nginx.conf /etc/nginx/nginx.conf
24
+ COPY mcp-proxy.conf /etc/nginx/mcp-proxy.conf
25
+ COPY scripts/run-space.py /usr/local/bin/run-space.py
26
  COPY scripts/start-homeassistant /usr/local/bin/start-homeassistant
27
  COPY scripts/backup-homeassistant.py /usr/local/bin/backup-homeassistant.py
28
  RUN chmod +x /usr/local/bin/start-homeassistant
README.md CHANGED
@@ -10,7 +10,7 @@ suggested_hardware: cpu-basic
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
 
@@ -34,4 +34,61 @@ Home account. This integration generally requires the Xiaomi cloud region that o
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  # Home Assistant on Hugging Face Spaces
12
 
13
+ This Space runs Home Assistant with Xiaomi Miot and the China-region Haier Smart Home custom integrations pre-installed.
14
 
15
  ## Required Space secrets
16
 
 
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.
38
+ ## Agent MCP 接入(同一个 Space)
39
+
40
+ 网页继续使用 Space 的直接应用地址;MCP 地址为该地址后加 /mcp。采用 Streamable HTTP,非旧版 SSE。
41
+
42
+ ### 你需要完成的步骤
43
+
44
+ 1. 在当前 Home Assistant 中确认配置已备份成功,再上传本项目文件。Dockerfile 已包含米家和海尔智家集成安装命令,无需手动合并。不要上传 .venv、tests、__pycache__ 或任何令牌。
45
+ 2. 创建专用于 Agent 的 Home Assistant 用户,以该用户登录,进入个人资料 → 长期访问令牌,生成令牌。非管理员用户权限仍可能涵盖大量设备;本项目按要求不设置设备或服务白名单。
46
+ 3. 在 Space Settings → Variables and secrets → Secrets 中设置 HA_TOKEN 为该长期令牌,MCP_API_KEY 为你生成的随机密钥(建议至少 32 字节随机值)。密钥不要包含换行。保留 HF_TOKEN 与 BACKUP_REPO_ID。可以先部署再创建 HA 令牌;未配置密钥时 MCP 默认拒绝请求,网页仍可使用。
47
+ 4. 等待重新构建完成,再在 Agent 中添加远程 MCP,URL 为 https://你的实际应用域名.hf.space/mcp,传输方式 Streamable HTTP,Header 为 Authorization: Bearer 你的MCP_API_KEY。Agent 不需要 HA_TOKEN。
48
+ 5. 使用 Agent 的连接测试,然后先调用 list_entities 或 list_services,再控制设备。
49
+
50
+ 通用配置示例(具体字段取决于 Agent 平台):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "home-assistant": {
56
+ "url": "https://YOUR-SPACE.hf.space/mcp",
57
+ "headers": {"Authorization": "Bearer YOUR_MCP_API_KEY"}
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ 可用工具:list_entities(domain 可选)、get_entity_state(entity_id)、list_services()、call_service(domain, service, service_data)。call_service 可以调用该 HA 用户有权执行的服务,包括自动化与脚本;没有实体白名单。它不提供任意路径的 HTTP 代理。服务失败会返回工具错误,不返回 HA 令牌。
64
+
65
+ MCP 采用独立 Bearer 密钥,不实现 OAuth;Agent 必须支持自定义 Authorization header。如果仅支持 OAuth 自动登录或旧版 HTTP+SSE,此配置不能直接接入。公开 Space 使用应用直接域名;私有 Space 还有 Hugging Face 平台鉴权,不能仅靠 MCP 密钥访问。
66
+
67
+ ### 运行与配置迁移
68
+
69
+ Nginx 对外监听 7860;HA 改为内部 8123;MCP 仅监听 127.0.0.1:8000。Nginx 支持 HA WebSocket 和 MCP 请求转发。启动时修改 configuration.yaml 中 inline http 配置的端口与代理信任列表,保留其他 YAML,包括 !include;http: !include 形式需要先手工改为内联映射。代理信任列表改为 loopback 和 10.0.0.0/8。
70
+
71
+ 首次无快照会继续初始化;Hub 恢复遇到权限或网络错误时停止,防止新空配置覆盖原备份。首次定时备份在启动约 60 秒后,此后每 10 分钟,正常停止时会尝试最后一次备份。强制终止仍可能丢失未上传配置。免费 Space 休眠时 MCP 和 HA 均不可用。
72
+
73
+ 排查:401 表示 MCP 密钥未匹配;503 表示 MCP_API_KEY 未配置;工具报 HA HTTP 401 表示 HA_TOKEN 无效;工具报连接错误或代理 502 可能是 HA 尚未就绪。浏览器直接打开 /mcp 没带鉴权时返回 401 属正常现象。
74
+
75
+ ## 直接上传文件到 Space
76
+
77
+ 一次提交上传以下文件,保留相对路径,不需要部署压缩包:
78
+
79
+ ```text
80
+ Dockerfile
81
+ README.md
82
+ .dockerignore
83
+ nginx.conf
84
+ mcp-proxy.conf
85
+ mcp_server/requirements.txt
86
+ mcp_server/server.py
87
+ scripts/start-homeassistant
88
+ scripts/run-space.py
89
+ scripts/backup-homeassistant.py
90
+ ```
91
+
92
+ 在 Space 的 Files 页面通过 Add file → Upload files 上传;请核对 scripts 与 mcp_server 下的文件未被放在仓库根目录。所有文件准备好后在同一次提交中保存,Space 将自动重新构建。不要上传 .venv、tests、__pycache__、.pytest_cache、.env 或旧压缩包。
93
+
94
+ 构建时安装海尔智家(sfairy/ha_haier),启动脚本会将镜像中缺少的集成复制到配置目录的 custom_components 中。启动后进入 设置 → 设备与服务 → 添加集成 → 搜索海尔智家,使用中国区海尔智家账号登录。已经恢复的同名集成目录会保留,不会强制覆盖其代码或账号配置。
mcp-proxy.conf ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ proxy_http_version 1.1;
2
+ proxy_set_header Host $http_host;
3
+ proxy_set_header Authorization $http_authorization;
4
+ proxy_set_header Connection "";
5
+ proxy_buffering off;
6
+ proxy_cache off;
7
+ proxy_read_timeout 3600s;
8
+ proxy_send_timeout 3600s;
mcp_server/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ mcp==1.30.0
2
+ uvicorn>=0.30,<1
3
+ httpx>=0.27,<1
mcp_server/server.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Authenticated Streamable HTTP bridge to Home Assistant."""
2
+ import os
3
+ import secrets
4
+ from typing import Any
5
+
6
+ import httpx
7
+ from mcp.server.fastmcp import FastMCP
8
+ from mcp.server.transport_security import TransportSecuritySettings
9
+ from starlette.responses import JSONResponse
10
+
11
+ HA_URL = os.getenv("HA_URL", "http://127.0.0.1:8123").rstrip("/")
12
+ HA_TOKEN = os.getenv("HA_TOKEN", "")
13
+ MCP_API_KEY = os.getenv("MCP_API_KEY", "")
14
+ mcp = FastMCP("Home Assistant", stateless_http=True, json_response=True,
15
+ transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False))
16
+
17
+ async def ha_request(method: str, path: str, data: dict | None = None) -> Any:
18
+ if not HA_TOKEN:
19
+ raise ValueError("Set the HA_TOKEN Space secret to a Home Assistant long-lived token.")
20
+ async with httpx.AsyncClient(timeout=45, trust_env=False) as client:
21
+ response = await client.request(method, HA_URL + "/api/" + path,
22
+ headers={"Authorization": "Bearer " + HA_TOKEN}, json=data)
23
+ if response.is_error:
24
+ raise ValueError(f"Home Assistant returned HTTP {response.status_code}.")
25
+ return response.json()
26
+
27
+ @mcp.tool()
28
+ async def list_entities(domain: str | None = None) -> list[dict]:
29
+ """List actual entity IDs, states and attributes. Optionally filter by domain (light, climate, etc.)."""
30
+ states = await ha_request("GET", "states")
31
+ return [s for s in states if domain is None or s["entity_id"].startswith(domain + ".")]
32
+
33
+ @mcp.tool()
34
+ async def get_entity_state(entity_id: str) -> dict:
35
+ """Read an entity's current state and attributes using its actual entity ID."""
36
+ validate_name(entity_id, dotted=True)
37
+ return await ha_request("GET", "states/" + entity_id)
38
+
39
+ @mcp.tool()
40
+ async def list_services() -> list[dict]:
41
+ """Discover available HA services and their parameters before controlling devices."""
42
+ return await ha_request("GET", "services")
43
+
44
+ @mcp.tool()
45
+ async def call_service(domain: str, service: str, service_data: dict[str, Any] | None = None) -> Any:
46
+ """Execute a Home Assistant service. This changes devices or automations. Use actual service names and entity IDs discovered by the read tools; service_data is the HA JSON body, e.g. {"entity_id":"light.room"}. No device whitelist is applied."""
47
+ validate_name(domain)
48
+ validate_name(service)
49
+ return await ha_request("POST", f"services/{domain}/{service}", service_data or {})
50
+
51
+ def validate_name(value: str, dotted: bool = False):
52
+ import re
53
+ pattern = r"[a-z0-9_]+\.[a-z0-9_]+" if dotted else r"[a-z0-9_]+"
54
+ if not re.fullmatch(pattern, value):
55
+ raise ValueError("Invalid Home Assistant identifier.")
56
+
57
+ class BearerAuth:
58
+ def __init__(self, app):
59
+ self.app = app
60
+
61
+ async def __call__(self, scope, receive, send):
62
+ if scope["type"] == "http":
63
+ if not MCP_API_KEY:
64
+ response = JSONResponse({"error": "MCP is disabled until MCP_API_KEY is configured"}, status_code=503)
65
+ return await response(scope, receive, send)
66
+ headers = dict(scope["headers"])
67
+ actual = headers.get(b"authorization", b"")
68
+ expected = ("Bearer " + MCP_API_KEY).encode()
69
+ if not secrets.compare_digest(actual, expected):
70
+ response = JSONResponse({"error": "Unauthorized"}, status_code=401,
71
+ headers={"WWW-Authenticate": "Bearer"})
72
+ return await response(scope, receive, send)
73
+ await self.app(scope, receive, send)
74
+
75
+ app = BearerAuth(mcp.streamable_http_app())
nginx.conf ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes 1;
2
+ pid /tmp/nginx.pid;
3
+ error_log /dev/stderr warn;
4
+ events { worker_connections 1024; }
5
+ http {
6
+ access_log off;
7
+ client_max_body_size 100m;
8
+ map $http_upgrade $connection_upgrade {
9
+ default upgrade;
10
+ '' close;
11
+ }
12
+ server {
13
+ listen 7860;
14
+ location = /mcp { proxy_pass http://127.0.0.1:8000; include /etc/nginx/mcp-proxy.conf; }
15
+ location /mcp/ { proxy_pass http://127.0.0.1:8000; include /etc/nginx/mcp-proxy.conf; }
16
+ location / {
17
+ proxy_pass http://127.0.0.1:8123;
18
+ proxy_http_version 1.1;
19
+ proxy_set_header Host $http_host;
20
+ proxy_set_header Upgrade $http_upgrade;
21
+ proxy_set_header Connection $connection_upgrade;
22
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23
+ proxy_set_header X-Forwarded-Proto https;
24
+ proxy_read_timeout 3600s;
25
+ }
26
+ }
27
+ }
scripts/backup-homeassistant.py CHANGED
@@ -48,21 +48,24 @@ def backup(config: Path) -> None:
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
 
 
48
 
49
 
50
  def restore(config: Path) -> None:
51
+ from huggingface_hub.errors import EntryNotFoundError
52
  try:
53
  _, repo_id, repo_type, token = hub_api()
54
  from huggingface_hub import hf_hub_download
55
  archive = hf_hub_download(repo_id=repo_id, filename=ARCHIVE_NAME,
56
  repo_type=repo_type, token=token, force_download=True)
57
+ except EntryNotFoundError:
58
+ print("No existing Hub snapshot; starting a new configuration.", flush=True)
59
  return
60
+ except Exception as exc:
61
+ raise RuntimeError("Hub restore failed; refusing to overwrite the remote backup with a fresh configuration.") from exc
62
  config.mkdir(parents=True, exist_ok=True)
63
  with tarfile.open(archive, "r:gz") as tar:
64
  root = config.resolve()
65
  for member in tar.getmembers():
66
  target = (config / member.name).resolve()
67
  if root not in target.parents: raise RuntimeError(f"Unsafe backup path: {member.name}")
68
+ tar.extractall(config, filter="data")
69
  print(f"Restored {ARCHIVE_NAME} into {config}", flush=True)
70
 
71
 
scripts/run-space.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prepare HA configuration and supervise the three Space services."""
2
+ import os
3
+ from pathlib import Path
4
+ import re
5
+ import shutil
6
+ import signal
7
+ import subprocess
8
+ import time
9
+
10
+ CONFIG = Path(os.getenv("HOME_ASSISTANT_CONFIG", "/data/home-assistant"))
11
+ BACKUP = "/usr/local/bin/backup-homeassistant.py"
12
+
13
+
14
+ def prepare_http(text):
15
+ # Change only the inline http mapping, preserving !include and other YAML elsewhere.
16
+ match = re.search(r"(?m)^http:[^\n]*(?:\n(?:[ \t]+[^\n]*|#[^\n]*|[ \t]*))*", text)
17
+ block = match.group(0) if match else "http:"
18
+ if block.splitlines()[0].strip() != "http:":
19
+ raise ValueError("http must be an inline YAML mapping in configuration.yaml; move its settings there before deployment.")
20
+ block = re.sub(r"(?m)^ (server_port|use_x_forwarded_for):[^\n]*\n?", "", block)
21
+ block = re.sub(r"(?m)^ trusted_proxies:[^\n]*(?:\n[ \t]{4,}[^\n]*)*\n?", "", block)
22
+ block = block.rstrip() + "\n server_port: 8123\n use_x_forwarded_for: true\n trusted_proxies:\n - 127.0.0.1\n - ::1\n - 10.0.0.0/8\n"
23
+ if match:
24
+ return text[:match.start()] + block + text[match.end():]
25
+ return text.rstrip() + "\n\n" + block
26
+
27
+
28
+ def backup(action):
29
+ result = subprocess.run(["python3", BACKUP, action, "--config", str(CONFIG)], timeout=120)
30
+ if result.returncode:
31
+ print(f"Backup operation {action} failed; check Hub secrets and permissions.", flush=True)
32
+
33
+
34
+ def main():
35
+ CONFIG.mkdir(parents=True, exist_ok=True)
36
+ config_file = CONFIG / "configuration.yaml"
37
+ if not config_file.exists():
38
+ backup("restore")
39
+ text = config_file.read_text() if config_file.exists() else "default_config:\n"
40
+ config_file.write_text(prepare_http(text))
41
+ components = CONFIG / "custom_components"
42
+ components.mkdir(exist_ok=True)
43
+ for source in Path("/opt/ha-custom-components").iterdir():
44
+ if not (components / source.name).exists():
45
+ shutil.copytree(source, components / source.name)
46
+
47
+ stopping = False
48
+ def stop(signum, frame):
49
+ nonlocal stopping
50
+ stopping = True
51
+ signal.signal(signal.SIGTERM, stop)
52
+ signal.signal(signal.SIGINT, stop)
53
+ processes = []
54
+ exit_code = 0
55
+ try:
56
+ for command in (
57
+ ["hass", "--config", str(CONFIG)],
58
+ ["/opt/mcp-venv/bin/python", "-m", "uvicorn", "server:app", "--app-dir", "/opt/mcp-server", "--host", "127.0.0.1", "--port", "8000", "--no-access-log"],
59
+ ["nginx", "-g", "daemon off;"],
60
+ ):
61
+ processes.append(subprocess.Popen(command, start_new_session=True))
62
+ next_backup = time.monotonic() + 60
63
+ enabled = bool(os.getenv("HF_TOKEN") and os.getenv("BACKUP_REPO_ID"))
64
+ while not stopping:
65
+ for process in processes:
66
+ if process.poll() is not None:
67
+ print(f"Service exited with code {process.returncode}; stopping container.", flush=True)
68
+ exit_code = process.returncode or 1
69
+ stopping = True
70
+ break
71
+ if enabled and not stopping and time.monotonic() >= next_backup:
72
+ try:
73
+ backup("backup")
74
+ except Exception as exc:
75
+ print(f"Periodic backup failed: {type(exc).__name__}", flush=True)
76
+ next_backup = time.monotonic() + 600
77
+ time.sleep(1)
78
+ finally:
79
+ for process in reversed(processes):
80
+ if process.poll() is None:
81
+ os.killpg(process.pid, signal.SIGTERM)
82
+ for process in processes:
83
+ try:
84
+ process.wait(timeout=25)
85
+ except subprocess.TimeoutExpired:
86
+ os.killpg(process.pid, signal.SIGKILL)
87
+ process.wait()
88
+ if os.getenv("HF_TOKEN") and os.getenv("BACKUP_REPO_ID"):
89
+ try:
90
+ backup("backup")
91
+ except Exception as exc:
92
+ print(f"Shutdown backup failed: {type(exc).__name__}", flush=True)
93
+ return exit_code
94
+
95
+ if __name__ == "__main__":
96
+ raise SystemExit(main())
scripts/start-homeassistant CHANGED
@@ -1,63 +1,3 @@
1
  #!/bin/sh
2
  set -eu
3
-
4
- CONFIG_DIR="${HOME_ASSISTANT_CONFIG:-/data/home-assistant}"
5
- CUSTOM_COMPONENT="/opt/ha-custom-components/xiaomi_miot"
6
- CONFIG_FILE="$CONFIG_DIR/configuration.yaml"
7
-
8
- mkdir -p "$CONFIG_DIR/custom_components"
9
- if [ ! -f "$CONFIG_FILE" ]; then
10
- python3 /usr/local/bin/backup-homeassistant.py restore --config "$CONFIG_DIR" || true
11
- fi
12
-
13
- if [ ! -d "$CONFIG_DIR/custom_components/xiaomi_miot" ]; then
14
- cp -a "$CUSTOM_COMPONENT" "$CONFIG_DIR/custom_components/xiaomi_miot"
15
- fi
16
-
17
- if [ ! -f "$CONFIG_FILE" ]; then
18
- cat > "$CONFIG_FILE" <<'YAML'
19
- default_config:
20
-
21
- http:
22
- server_port: 7860
23
- use_x_forwarded_for: true
24
- trusted_proxies:
25
- - 10.0.0.0/8
26
- YAML
27
- elif ! grep -q 'use_x_forwarded_for:' "$CONFIG_FILE"; then
28
- if grep -q '^ server_port: 7860' "$CONFIG_FILE"; then
29
- sed -i '/^ server_port: 7860/a\ use_x_forwarded_for: true\n trusted_proxies:\n - 10.0.0.0/8' "$CONFIG_FILE"
30
- else
31
- cat >> "$CONFIG_FILE" <<'YAML'
32
-
33
- http:
34
- server_port: 7860
35
- use_x_forwarded_for: true
36
- trusted_proxies:
37
- - 10.0.0.0/8
38
- YAML
39
- fi
40
- fi
41
-
42
- if [ -n "${HF_TOKEN:-}" ] && [ -n "${BACKUP_REPO_ID:-}" ]; then
43
- (
44
- while sleep 600; do
45
- python3 /usr/local/bin/backup-homeassistant.py backup --config "$CONFIG_DIR" || true
46
- done
47
- ) &
48
- BACKUP_PID=$!
49
- else
50
- BACKUP_PID=""
51
- fi
52
-
53
- cleanup() {
54
- if [ -n "$BACKUP_PID" ]; then
55
- kill "$BACKUP_PID" 2>/dev/null || true
56
- fi
57
- if [ -n "${HF_TOKEN:-}" ] && [ -n "${BACKUP_REPO_ID:-}" ]; then
58
- python3 /usr/local/bin/backup-homeassistant.py backup --config "$CONFIG_DIR" || true
59
- fi
60
- }
61
- trap cleanup EXIT TERM INT
62
-
63
- exec hass --config "$CONFIG_DIR"
 
1
  #!/bin/sh
2
  set -eu
3
+ exec python3 /usr/local/bin/run-space.py