# Home Automation Backend 这是当前 `home-automation` 项目的首个 Python 版本。 当前系统已经包含: - FastAPI Web 应用与服务端模板页面 - SQLite + SQLAlchemy + Alembic 的三库结构 - username/password + server-side session 鉴权 - runtime config 页面与 app DB 持久化 - public IPv4 monitor、历史持久化与定时检查 - SMTP 配置、测试发信与 public IPv4 changed 邮件通知 - location recorder - poo recorder - Home Assistant inbound / outbound integration - TickTick OAuth 与 action task 集成 - pytest 测试与 OpenAPI 导出脚本 - Docker / Compose 部署入口 当前明确不包含: - Notion 模块 ## 当前配置现实 当前系统仍然是三个独立的 SQLite 数据库文件,而不是单一数据库: - `app` 级共享数据使用自己的 DB 文件 - `location` 模块使用自己的 DB 文件 - `poo` 模块使用自己的 DB 文件 当前阶段明确不借这次重构把这些 DB 合并。配置层已经显式反映这一点: - `APP_DATABASE_URL` - `LOCATION_DATABASE_URL` - `POO_DATABASE_URL` 目前 auth、`location` 和 `poo` 都已经接到各自独立的数据库文件。 其中 `app` 级共享 DB 当前主要用于: - 单个 admin 用户 - server-side session - runtime config 持久化 - public IPv4 当前状态与变化历史 这部分现在也使用 Alembic 管理: - `app db` 不会在应用启动时自动创建 - 需要先运行 `python scripts/app_db_adopt.py` - 这个脚本会创建新 DB 并建好 schema ## 当前目录 主要目录如下: - `app/`: FastAPI 应用代码 - `alembic_app/`: App DB 的 Alembic migration 环境 - `alembic_location/`: Location DB 的 Alembic migration 环境 - `alembic_poo/`: Poo DB 的 Alembic migration 环境 - `tests/`: pytest 测试 - `docs/`: 当前系统说明文档 - `scripts/`: 辅助脚本,例如 OpenAPI 导出 ## 依赖管理 项目现在采用 `pip-tools` 管理依赖: - 生产依赖源文件:`requirements.in` - 开发依赖源文件:`dev-requirements.in` - 编译产物: - `requirements.txt` - `dev-requirements.txt` 更新依赖时建议使用: ```bash python -m venv .venv source .venv/bin/activate pip install pip-tools pip-compile requirements.in pip-compile dev-requirements.in ``` 如果要升级某个依赖,可以用: ```bash pip-compile --upgrade-package fastapi requirements.in pip-compile dev-requirements.in ``` ## 本地启动 建议使用 Python 3.11 或以上版本。 1. 创建虚拟环境并安装依赖 ```bash python -m venv .venv source .venv/bin/activate pip install -r dev-requirements.txt ``` 2. 准备环境变量 ```bash cp .env.example .env ``` 3. 初始化数据库 ```bash python -m scripts.run_migrations ``` 4. 启动服务 ```bash uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` 启动后可访问: - 应用首页:`http://localhost:8000/` - 健康检查:`http://localhost:8000/status` - Swagger UI:`http://localhost:8000/docs` - ReDoc:`http://localhost:8000/redoc` ## 数据库与 Alembic 当前默认使用 SQLite,并区分三个数据库文件: - App DB:`sqlite:///./data/app.db` - Location DB:`sqlite:///./data/locationRecorder.db` - Poo DB:`sqlite:///./data/pooRecorder.db` - 数据目录:`./data/` 初始化 migration 环境后,可继续添加模型并生成迁移: 当前 `app`、`location` 和 `poo` 都已经有各自独立的 Alembic 链路。 - App Alembic 环境:`alembic_app.ini` + `alembic_app/` - Location Alembic 环境:`alembic_location.ini` + `alembic_location/` - Poo Alembic 环境:`alembic_poo.ini` + `alembic_poo/` - 统一 migration job:`python -m scripts.run_migrations` - App DB 初始化:`python scripts/app_db_adopt.py` - Location DB 接管 / 初始化:`python scripts/location_db_adopt.py` - Poo DB 接管 / 初始化:`python scripts/poo_db_adopt.py` ## 基础鉴权 当前项目提供一个单用户 admin 鉴权层,用于保护配置页面与管理能力。 - 认证模型:`username/password` - 会话模型:server-side session + cookie - 当前主要受保护页面:`/config` - 当前公开页面:`/login` - 当前公开 API:现有业务 API 暂未在这一轮统一收口到 auth 下 安全实现的当前边界: - 密码使用 Argon2 做哈希存储 - session cookie 使用 `HttpOnly` - `Secure` 默认随 `APP_ENV` 切换:非 development 时默认开启 - `SameSite=Lax` - 登录表单和登出表单都有基础 CSRF 防护 首次启动时,如果 `APP_DATABASE_URL` 对应的 auth DB 里还没有用户,应用会使用: - `AUTH_BOOTSTRAP_USERNAME` - `AUTH_BOOTSTRAP_PASSWORD` 创建初始 admin 用户。当前默认就是: - username: `admin` - password: `admin` 首次登录后会被要求立即修改密码。这个 bootstrap 只用于首个用户落库,不是后续的完整配置管理方案。 当前前端主要有两条页面路径: - `/login` - `/config` 无论是本地 `host:port` 还是反向代理后的域名访问,登录成功后都使用相对路径跳转到 `/config`。 ## Config 持久化 当前 config 页面不会把修改写回 `.env`。 当前原则是: - `.env` 只负责 bootstrap / fallback - app 启动先从 `.env` 读取数据库地址等基础配置 - 请求期读取配置时,优先使用 app DB 中的 `app_config` 表 - 如果数据库里没有对应值,再 fallback 到 `.env` 这意味着: - location / poo / app DB 地址仍然属于 bootstrap 范畴 - 运行时可编辑配置主要通过 `app_config` 表持久化 - token / secret 这类运行时必须可取回的配置,目前允许明文存储在 config 表中 - 登录密码仍然单独使用 Argon2 哈希,不走 config 表明文存储 当前已经接入 config 页面的运行时配置包括: - 基础系统配置 - auth cookie 相关配置 - SMTP 基础配置 - TickTick OAuth 配置 - Home Assistant 配置 其中 SMTP password 与其他 secret 字段一致: - 页面不明文回显 - 留空提交时保留旧值 - 用于测试发信与自动通知时不会写入响应 ## Public IPv4 Monitor 当前系统已经提供最小可用的 public IPv4 monitor: - 使用单一 provider 检查当前公网 IPv4 - 将状态与变化历史持久化到 app DB - 提供受保护的手动检查入口:`GET /public-ip/check` - 启动时注册 APScheduler job,默认每 4 小时检查一次 当前 app DB 中与此功能相关的新表: - `public_ip_state` - `public_ip_history` 状态语义如下: - `first_seen`:首次发现当前公网 IPv4 - `unchanged`:与上次状态一致 - `changed`:公网 IPv4 发生变化 - `error`:provider 请求失败或返回无效值 ## SMTP 与邮件通知 当前系统已经提供最小可用的 SMTP 能力: - SMTP 配置可在 `/config` 页面填写并保存到 `app_config` - 可通过 config 页面发送测试邮件 - 邮件 `From` 头支持显示名,例如 `Home Automation ` 当前 SMTP 配置项包括: - `SMTP_ENABLED` - `SMTP_HOST` - `SMTP_PORT` - `SMTP_USERNAME` - `SMTP_PASSWORD` - `SMTP_FROM_NAME` - `SMTP_FROM_ADDRESS` - `SMTP_TO_ADDRESS` - `SMTP_USE_STARTTLS` 当前 public IPv4 monitor 已与 SMTP sender 接通,但只处理一个很小的通知场景: - 当 public IPv4 check 结果为 `changed` 时,自动发送一封英文纯文本邮件 以下情况不会发邮件: - `first_seen` - `unchanged` - `error` 当前通知邮件内容固定,不提供模板系统,正文会包含: - previous IP - current IP - detected time 手动测试时,如果需要再次模拟一次 IP 变化,可以临时修改 `public_ip_state.current_ipv4` 为一个保留测试地址,然后再次调用 `GET /public-ip/check`。 ## OpenAPI 可使用下面的脚本重新导出当前 API 定义: ```bash python scripts/export_openapi.py ``` 导出结果会写入: - `openapi/openapi.json` - `openapi/openapi.yaml` ## Docker Compose 当前默认 Compose 服务名为 `app`,容器名固定为 `home-automation-app`。 当前 Compose 分成两层: - `docker-compose.yml`:默认使用 registry image,适合部署 / 生产拉取 - `docker-compose.override.yml`:仅为本地开发追加 `build: .` 本地开发启动方式: ```bash docker compose up -d --build ``` 上面的命令会自动叠加 `docker-compose.override.yml`,因此本地仍然会按当前工作目录重新 build。 如果要按生产方式直接从 registry 拉取并启动,显式只使用基础 compose 文件: ```bash docker compose -f docker-compose.yml pull docker compose -f docker-compose.yml up -d ``` 持续查看日志: ```bash docker compose logs -f app ``` ## Grafana Provisioning 当前仓库支持通过 Grafana provisioning 自动加载 SQLite datasource 和 repo 内的 dashboard 导出文件。 需要保留的文件路径如下: - `grafana/provisioning/datasources/locationrecorder.yaml` - `grafana/provisioning/datasources/poorecorder.yaml` - `grafana/provisioning/dashboards/provider.yaml` - `grafana/dashboards/locationrecorder.json` - `grafana/dashboards/poorecorder.json` 这些文件的职责分别是: - `grafana/provisioning/datasources/locationrecorder.yaml`:声明 `locationrecorder` SQLite datasource,并指向 `/data/home-automation/locationRecorder.db` - `grafana/provisioning/datasources/poorecorder.yaml`:声明 `poorecorder` SQLite datasource,并指向 `/data/home-automation/pooRecorder.db` - `grafana/provisioning/dashboards/provider.yaml`:告诉 Grafana 从 `/var/lib/grafana/dashboards` 扫描并加载 dashboard JSON - `grafana/dashboards/locationrecorder.json`:location recorder dashboard 导出文件,内容本身不需要在 compose 中改写 - `grafana/dashboards/poorecorder.json`:poo recorder dashboard 导出文件,内容本身不需要在 compose 中改写 当前 `docker-compose.yml` 中,Grafana service 需要挂载以下目录: - `./grafana/provisioning -> /etc/grafana/provisioning:ro` - `./grafana/dashboards -> /var/lib/grafana/dashboards:ro` 同时保留现有 named volume `homeautomation_grafana_storage:/var/lib/grafana` 作为 Grafana 运行态数据存储。 一键启动前,至少需要以下文件已经存在: - `grafana/provisioning/datasources/locationrecorder.yaml` - `grafana/provisioning/datasources/poorecorder.yaml` - `grafana/provisioning/dashboards/provider.yaml` - `grafana/dashboards/locationrecorder.json` - `grafana/dashboards/poorecorder.json` 启动方式: ```bash docker compose up -d ``` 启动后会发生的事情: - Grafana 容器会安装 `frser-sqlite-datasource` 插件 - Grafana 会读取 `/etc/grafana/provisioning/datasources/` 下的 datasource YAML - Grafana 会读取 `/etc/grafana/provisioning/dashboards/provider.yaml` - Grafana 会从 `/var/lib/grafana/dashboards/` 自动导入两个 dashboard JSON - 现有 Grafana named volume 继续负责保存 Grafana 运行态数据,不会覆盖 repo 内的 dashboard 与 provisioning 文件 ## Container Image CI 项目提供了一个 release image workflow: - workflow 文件:`.github/workflows/docker-image.yml` - 触发条件:push 匹配 `v*` 的 tag,例如 `v1.0.0` - registry:`code.wanderingbadger.dev` - image:`code.wanderingbadger.dev//` `docker-compose.yml` 中生产默认使用的 app image 当前为: - `code.wanderingbadger.dev/tliu93/home-automation:latest` 当前 workflow 不再把 image name 硬编码到特定 user package 路径,而是直接使用当前仓库标识生成镜像路径: - `code.wanderingbadger.dev/${github.repository}:${tag}` 在 Gitea 这里,package 更贴近 repo 归属的语义,主要体现在镜像命名路径本身,而不是额外的“绑定”动作。也就是说,当前发布方式是按仓库路径约定来对齐 repo/package 语义。 这个 workflow 会构建并推送 multi-arch image: - `linux/amd64` - `linux/arm64` 推送的 tag: - release tag 本身,例如 `v1.0.0` - `latest` workflow 依赖以下 secrets: - `REGISTRY_USERNAME` - `REGISTRY_TOKEN` CI 产出的 image 是给部署机直接 `docker pull` 使用的。部署机不需要 checkout 本仓库,也不需要本地执行 `docker build`。 ## 运行测试 ```bash pytest ``` 当前测试包含: - app 基本启动测试 - `/status` endpoint 测试 - 登录 / session 基础流程测试 ## OpenAPI 导出 FastAPI 默认会暴露 OpenAPI。若需要导出静态 schema 文件,可运行: ```bash python scripts/export_openapi.py ``` 输出文件会写到: - `openapi/openapi.json` - `openapi/openapi.yaml` `openapi/` 当前纳入版本控制。接口发生变更时,应重新运行导出脚本并同步提交生成的 schema 文件。 ## 容器启动 1. 准备环境变量文件 ```bash cp .env.example .env ``` 2. 启动容器 ```bash docker compose up --build ``` 默认端口: - `8000:8000` SQLite 持久化目录: - 本地 `./data` - 容器内 `/app/data`