Files
home-automation/README.md
T
tliu93 da236643f2
frontend / frontend (push) Successful in 2m0s
pytest / test (push) Successful in 1m32s
M2: frontend walkthrough fixes + explicit dev compose stack
Post-M2 self-walkthrough polish, batched into one commit.

Map / heat:
- fix heat-layer white-screen crash after login (add layer to map before
  setLatLngs; an off-map leaflet.heat layer has a null _map and throws)
- normalize each heat layer to the densest pixel cell visible in the CURRENT
  viewport (maxZoom:0 so intensity factor f=1) and recompute on moveend/zoomend,
  so sparse poo data reaches red and stays normalized at any zoom level
- dark CARTO basemap tiles when the color scheme is dark

UI:
- dark-mode toggle in the top-right, beside the settings gear
- switch top-right nav (records / theme / settings / logout) to Feather icons
  with hover tooltips
- home: Grafana-style quick time-range presets + back/forward shift buttons,
  placed between the From/To pickers and Apply; fix Select/tooltip z-index
  (Leaflet stacking) and the shift-button height alignment

API client:
- stop flooding GET /api/session with 401s: the session probe and the login
  endpoint own their 401s (no global redirect), which fixes the logout hang and
  the spinning login page

Compose:
- rename docker-compose.override.yml -> docker-compose.dev.yml as an explicit,
  non-auto-layered dev stack (8001, -dev container names, prod-copy ./data DB);
  update tests/test_deployment.py (read dev.yml, tolerate the !override tag) and
  the README "Docker Compose" section

Tests:
- pixel-grid peak counter, time-range presets, heat-layer ordering regression,
  and 401-redirect regression
2026-06-13 15:20:50 +02:00

13 KiB
Raw Permalink Blame History

Home Automation Backend

这是当前 home-automation 项目的首个 Python 版本。

当前系统已经包含:

  • FastAPI Web 应用(React SPA 前端 + JSON API
  • 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),所有数据表都在其中:

  • auth(单个 admin 用户、server-side session
  • runtime config 持久化(app_config 表)
  • public IPv4 当前状态与变化历史
  • location 记录(location 表)
  • poo 记录(poo_records 表)

配置层只保留一个数据库环境变量:

  • APP_DATABASE_URL

app.db 不会在应用启动时自动创建,需要先运行:

python -m scripts.run_migrations

该命令会通过 Alembic 将 app.db 初始化或升级到最新 head(含 location / poo_records 表)。

当前目录

主要目录如下:

  • app/: FastAPI 应用代码(包含 JSON API、业务服务、数据模型)
  • frontend/: React SPA 前端(Vite + React + TypeScript + Mantine
  • alembic_app/: App DB 的 Alembic migration 环境(同时管理 location / poo_records 表)
  • tests/: pytest 测试
  • docs/: 当前系统说明文档
  • scripts/: 辅助脚本,例如 OpenAPI 导出
  • openapi/: OpenAPI schema 静态产物(openapi.json / openapi.yaml),纳入版本控制

依赖管理

项目现在采用 pip-tools 管理依赖:

  • 生产依赖源文件:requirements.in
  • 开发依赖源文件:dev-requirements.in
  • 编译产物:
    • requirements.txt
    • dev-requirements.txt

更新依赖时建议使用:

python -m venv .venv
source .venv/bin/activate
pip install pip-tools
pip-compile requirements.in
pip-compile dev-requirements.in

如果要升级某个依赖,可以用:

pip-compile --upgrade-package fastapi requirements.in
pip-compile dev-requirements.in

本地启动

建议使用 Python 3.11 或以上版本。

  1. 创建虚拟环境并安装依赖
python -m venv .venv
source .venv/bin/activate
pip install -r dev-requirements.txt
  1. 准备环境变量
cp .env.example .env
  1. 初始化数据库
python -m scripts.run_migrations
  1. 启动服务
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

启动后可访问:

  • 应用首页(React SPA):http://localhost:8000/
  • 健康检查:http://localhost:8000/status
  • Swagger UIhttp://localhost:8000/docs
  • ReDochttp://localhost:8000/redoc

前端 v2React SPA

M2 用 React SPA 取代了原有 Jinja 服务端模板,由 FastAPI 同源托管(同一容器、同一 origin)。

技术栈

  • Vite + React + TypeScript + Mantine(组件库)
  • TanStack Query(数据请求/缓存)
  • Leaflet / react-leaflet(地图与热力图)
  • openapi-typescript + openapi-fetch(类型化 API client,由 openapi/openapi.json 生成)

本地开发(前端)

前端开发服务器会把 /api/location/poo/public-ip/homeassistant/ticktick/status 等路径代理到后端 FastAPI:8000)。

cd frontend
npm install
npm run dev          # 启动 Vite dev server(默认 :5173),代理后端

构建

cd frontend
npm run build        # 产出 frontend/dist

FastAPI 启动时若 frontend/dist/index.html 存在,则自动挂载该目录,并对非 /api 路径做 SPA fallback(返回 index.html)。该路径可通过环境变量 SPA_DIST_DIR 覆盖(默认值为 frontend/dist,与多阶段 Dockerfile 中 COPY/app/frontend/dist 一致)。

类型化 API Client

前端 API client 由后端 OpenAPI schema 自动生成:

cd frontend
npm run codegen      # 从 ../openapi/openapi.json 生成 src/api/schema.d.ts

生成物(src/api/schema.d.ts)已提交入库,CI 会校验它与 openapi/openapi.json 保持同步。

前端校验闸门

cd frontend
npm run lint         # ESLint
npm run typecheck    # TypeScript 类型检查
npm run test         # Vitest 单元测试
npm run build        # 构建,确认产出 dist

数据库与 Alembic

当前使用单一 SQLite 数据库文件:

  • App DBsqlite:///./data/app.db
  • 数据目录:./data/

所有模型(auth / config / public_ip / location / poo)共用同一个 Base,均通过单一 Alembic 链管理:

  • Alembic 环境:alembic_app.ini + alembic_app/
  • 统一 migration jobpython -m scripts.run_migrations
  • App DB 接管 / 初始化:python scripts/app_db_adopt.py

历史 location / poo 数据(旧版本遗留的独立 DB 文件)已通过以下脚本一次性迁移至 app.db(幂等,不删除旧文件):

python -m scripts.migrate_legacy_data

基础鉴权

当前项目提供一个单用户 admin 鉴权层,用于保护配置页面与管理能力。

  • 认证模型:username/password
  • 会话模型:server-side session + cookie
  • 当前受保护入口:React SPA/ 等客户端路由)调用 /api/* JSON 端点
  • 当前公开页面:/loginSPA 登录页)
  • 当前公开 API:裸 ingestion 端点(/location/record/poo/record 等设备调用端点)暂未收口到 session 保护(M3 再做)

安全实现的当前边界:

  • 密码使用 Argon2 做哈希存储
  • session cookie 使用 HttpOnly
  • Secure 默认随 APP_ENV 切换:非 development 时默认开启
  • SameSite=Lax
  • 写请求(POST/PUT/PATCH/DELETE)需携带 X-CSRF-Token headerSameSite=Lax + 自定义 header 纵深防御,无需 per-session token 值比对)

首次启动时,如果 APP_DATABASE_URL 对应的 auth DB 里还没有用户,应用会使用:

  • AUTH_BOOTSTRAP_USERNAME
  • AUTH_BOOTSTRAP_PASSWORD

创建初始 admin 用户。当前默认就是:

  • username: admin
  • password: admin

首次登录后会被要求立即修改密码。这个 bootstrap 只用于首个用户落库,不是后续的完整配置管理方案。

React SPA 主要页面路由(客户端路由,均由 FastAPI fallback 到 index.html):

  • /login:登录页
  • /:首页(地图热力图主视图)
  • /config:配置页(取代原 Jinja /config
  • /records:记录管理列表页

无论是本地 host:port 还是反向代理后的域名访问,登录成功后进入 SPA 首页(/)。

Config 持久化

当前 config 页面不会把修改写回 .env

当前原则是:

  • .env 只负责 bootstrap / fallback
  • app 启动先从 .env 读取数据库地址等基础配置
  • 请求期读取配置时,优先使用 app DB 中的 app_config
  • 如果数据库里没有对应值,再 fallback 到 .env

这意味着:

  • app DB 地址(APP_DATABASE_URL)仍然属于 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 配置可在 React SPA /config 页面填写并保存到 app_config(通过 PUT /api/config
  • 可通过 config 页面发送测试邮件(POST /api/config/smtp/test
  • 邮件 From 头支持显示名,例如 Home Automation <sender@example.com>

当前 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 定义:

python scripts/export_openapi.py

导出结果会写入:

  • openapi/openapi.json
  • openapi/openapi.yaml

Docker Compose

当前默认 Compose 服务名为 app,容器名固定为 home-automation-app

当前 Compose 分成两层:

  • docker-compose.yml:默认使用 registry image,适合部署 / 生产拉取(暴露 8881)
  • docker-compose.dev.yml:本地开发显式叠加层——追加 build: .、独立 project / 容器名(-dev 后缀)、暴露 8001,并把 DB 指向挂载的 ./data 副本,可与生产栈在同一台机器上并存

本地开发启动方式(显式叠加 dev 层):

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

dev 层刻意不沿用 docker-compose.override.yml 这种会被 docker compose up 自动叠加的文件名, 因此默认的 docker compose up 只用生产基础文件,不会把开发端口 / 配置误带到生产。

如果要按生产方式直接从 registry 拉取并启动,使用基础 compose 文件:

docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d

持续查看日志:

docker compose logs -f app

Container Image CI

项目提供了一个 release image workflow

  • workflow 文件:.github/workflows/docker-image.yml
  • 触发条件:push 匹配 v* 的 tag,例如 v1.0.0
  • registrycode.wanderingbadger.dev
  • imagecode.wanderingbadger.dev/<owner>/<repo>

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

运行测试

pytest

当前测试包含:

  • app 启动与 /status 检查
  • 登录 / session / 鉴权流程
  • runtime config 读写
  • public IPv4 monitor
  • SMTP 配置与测试发信
  • location / poo recorder 端点
  • Home Assistant inbound 集成
  • TickTick OAuth
  • 部署与迁移(run_migrations
  • legacy 数据迁移脚本(migrate_legacy_data

OpenAPI 导出

FastAPI 默认会暴露 OpenAPI。若需要导出静态 schema 文件,可运行:

python scripts/export_openapi.py

输出文件会写到:

  • openapi/openapi.json
  • openapi/openapi.yaml

openapi/ 当前纳入版本控制。接口发生变更时,应重新运行导出脚本并同步提交生成的 schema 文件。

容器启动

  1. 准备环境变量文件
cp .env.example .env
  1. 启动容器
docker compose up --build

默认端口:

  • 8000:8000

SQLite 持久化目录:

  • 本地 ./data
  • 容器内 /app/data