Files
home-automation/docs/architecture-overview.md
T

12 KiB
Raw Blame History

Python 骨架架构概览

本文档说明当前 Python skeleton 的职责边界与目录组织。它描述的是“后续迁移承载体”,不是完整业务实现。

当前目标

这一轮的目标是提供一个稳定、轻量、可持续扩展的基础工程,使后续可以逐步迁移:

  • TickTick integration
  • Home Assistant integration
  • poo records
  • location / life trajectory

目录设计

app/

应用核心代码目录。

  • main.py
    • FastAPI app factory
    • lifespan(APScheduler 启停、MQTT 客户端起停、连接后触发 HA Discovery 发布;注册 DSMR MQTT source,并启动/关闭每个 enabled WarmteLink 业务只读 serial worker;tibber-refresh、electricity 与 thermal cost tick 均由 scheduler 驱动)
    • 基础路由注册
  • config.py
    • 环境变量驱动的 settings(含 M5 新增的 MQTT/HA Discovery/Modbus 配置项;M6 新增 dsmr_ingest_enabled、dsmr_mqtt_topic、dsmr_sample_interval_s、tibber_api_token(secret)、tibber_home_id)
  • db.py
    • 统一数据层:一个 Base、一个绑定 app_database_url 的 cached engine(SQLite WAL)、get_engine / get_session_local / reset_db_caches / get_db_session
  • dependencies.py
    • 通用依赖注入
  • api/
    • HTTP routes
    • api/routes/api/:JSON API(/api/* 前缀),供 React SPA 调用:会话/鉴权、配置读写、记录 CRUD、Modbus、Expose 与 MQTT 测试;Energy 包含 source profile/source/channel/history/discover/binding/Meter API、scope-aware contracts/prices/costs,以及兼容的 DSMR latest API
    • 裸 ingestion 端点:GET /public-ip/check、POST /homeassistant/publish、POST /poo/record、GET /poo/latest、TickTick OAuth 等
  • models/
    • SQLAlchemy models
    • 所有模型(auth / config / public_ip / location / poo / modbus / expose / energy / meter_source)共用同一个 Base,均落在单一 app.db 中
    • M5 新增:ModbusDevice(设备部署层)、ModbusReading(通用遥测,JSON payload)、ExposedEntityToggle(HA 实体暴露开关)
    • Energy:MeterSource / MeterSourceChannel / MeterSourceBinding 将协议连接、稳定测量 channel 与 Meter epoch 分离;DsmrReading、WarmteLinkReading 分别保存 JSON 与 Decimal scalar 历史;electricity EnergyCostPeriod 绑定 source binding,thermal MeterCostPeriod 保存审计账本;合同按 electricity / thermal scope 共存
  • schemas/
    • Pydantic schemas(包括 modbus.py、expose.py、energy_contract.py、energy.py、meter_source.py)
  • services/
    • 业务服务层
    • 当前已迁入 config page 的 DB 持久化逻辑
    • 当前已迁入 public IPv4 检查、状态持久化与变化通知逻辑
    • 当前已迁入 SMTP 发信与测试发信逻辑
    • M5 新增:modbus_poll.py(采集 service,逐设备 poll + 落库 + 推 MQTT state)、ha_discovery.py(构建 HA Discovery payload、发布 retained config、发布 state)
    • Energy:dsmr_ingest.py 按 source 入库;warmtelink_ingest.py 接纳连续确认的业务只读 P1 scalar,warmtelink_worker.py 管理 interruptible serial reconnect(pyserial 的 POSIX O_RDWR 打开由非 root、非 privileged、无 m 的 Docker rw device rule 支持;worker 只 read/close,绝不 write);energy_cost.py 与 meter_cost.py 分别计算 electricity/thermal 账本,均拒绝跨 Meter/binding 相减
  • integrations/
    • 外部系统适配层
    • Home Assistant outbound adapter(REST 通道,原有)
    • M5 新增:modbus/(pymodbus 薄封装:driver.py 块读 + float32 解码;profiles.py YAML profile 加载/校验/解码;profiles/sdm120.yaml SDM120 协议声明)
    • M5 新增:mqtt.py(paho-mqtt 长连接 MqttManager:lifespan 起/停、配置变更重连、publish(topic, payload, retain))
    • M5 新增:expose.py(通用 expose 框架:ExposableEntity、provider 注册表、build_catalog;Modbus provider 从 YAML profile 派生 sensor/binary_sensor 实体目录)
    • M6 新增:pricing/(通用电价层:profiles.py pydantic 加载/校验 YAML profile + validate_values;strategies.py manual/tibber 出价策略注册表;profiles/manual.yaml 固定/双费率结构;profiles/tibber.yaml 动态电价结构)
    • M6 新增:tibber/(client.py:httpx GraphQL 客户端,priceInfoRange(QUARTER_HOURLY) 查询,按 starts_at 解析,不假设固定节点数)
    • M6 扩展:mqtt.py 新增订阅端(subscribe(topic, handler),on_connect 里 subscribe,on_message 按 topic 分发,handler 异常吞掉不崩连接)
    • M6 扩展:expose.py 新增 _energy_cost_provider(buy_price_now、sell_price_now、import_cost_total/export_revenue_total 均 total_increasing,反哺 HA Energy)
  • static/
    • 极简静态资源

alembic_app/

App DB 的唯一 Alembic migration 链,同时管理 location / poo_records 表。M1 将三个独立 DB 合并进 app.db 后,alembic_location/ 与 alembic_poo/ 已退役,全部由此链统一管理。

tests/

pytest 测试目录。后续可以在这里自然扩展:

  • unit tests
  • mock tests
  • integration tests

frontend/

React SPA 前端(M2 引入)。Vite + React + TypeScript + Mantine,由 FastAPI 同源托管。

  • src/:React 源码
  • src/api/:由 openapi/openapi.json 生成的类型化 client(schema.d.ts)+ fetch 封装
  • dist/:npm run build 产物,由 FastAPI 的 SPA_DIST_DIR 挂载并对非 /api 路径做 fallback

scripts/

辅助脚本目录。当前包含:

  • export_openapi.py:导出 OpenAPI schema 静态产物
  • run_migrations.py:运行 Alembic migration
  • app_db_adopt.py:App DB 接管 / 初始化
  • migrate_legacy_data.py:一次性历史数据搬迁脚本
  • admin_cli.py:Admin CLI 逃生通道(M4),见下方"登录加固"说明
  • modbus_cli.py(M5):Modbus 手工试读 CLI(read/probe 两个只读子命令),不依赖 DB,供受控手工验证链路连通性

openapi/

OpenAPI schema 静态产物(openapi.json / openapi.yaml),由 python scripts/export_openapi.py 生成,纳入版本控制。前端 codegen 以此为契约源。

登录加固(M4)

M4 在基础 Argon2 + server-side session 鉴权之上叠加了三层防御:

防爆破 / 指数退避:app/services/login_throttle.py 按 client IP 与 username 双键记失败计数,失败超过 3 次后指数增长等待时间(最长 15 分钟),POST /api/auth/login 在退避窗口内直接返回 429 + Retry-After,不执行 Argon2 验证。成功登录后清零。退避是延迟而非永久封号;全局开关 AUTH_LOGIN_THROTTLE_ENABLED(CONFIG_FIELDS,默认开);反代后需设 AUTH_TRUST_FORWARDED_FOR=true(.env 部署级,默认 false)。

CLI 逃生通道:scripts/admin_cli.py(入口 python -m scripts.admin_cli)直连本地 DB,无需 HTTP 服务运行、无需任何已存凭据,支持:重置密码(reset-password)、解锁退避(unlock)、关停 TOTP(disable-totp,零凭据最终逃生)、重新发放 TOTP secret(reissue-totp)、查看用户列表(list-admin)。CLI 只动 auth 行,不触碰用户数据表。

可选 TOTP 二次验证:admin 可在设置页自选启用 RFC 6238 TOTP。启用后登录为两步(密码 → 6 位动态码或一次性恢复码);不启用维持纯密码。TOTP secret 明文存库(与其他 secret 一致,靠文件权限保护);恢复码以 Argon2 哈希存储,使用后消费(一次性)。后端使用 pyotp,二维码由前端 qrcode.react 渲染。issuer 标签由 AUTH_TOTP_ISSUER(.env 部署级)配置,默认回退 app_name。

详细说明:docs/auth.md

M5 — 通用 Modbus 采集链路与 MQTT 通道

Modbus 采集链路

YAML profile(协议知识)         modbus_device 行(部署信息,DB)
  - 寄存器块/地址/类型             - host / port(网关 IP)
  - 每量:key/unit/device_class    - unit_id(Modbus slave 地址)
  - ha_component                   - friendly_name / profile / poll_interval_s / enabled
          │                                    │
          └────────────┬───────────────────────┘
                       │ APScheduler job(轮询所有 enabled 设备)
                       │  driver.py: ModbusTcpClient → FC04 块读 → 大端 float32 解码
                       │  profiles.py: decode(profile, registers) → dict[key → value]
                       ▼
              modbus_reading 行
              device_id · recorded_at · payload(JSON)
              {"voltage": 230.2, "current": 1.3, "active_power": 295.0, ...}

关键设计决策:

  • 命名分层:存储/采集/API 全部通用 modbus_*(/api/modbus/devices);面向用户的领域视图叫 Energy(第一个)。接入新设备型号只需新增 YAML profile,不改表/不改 API。
  • 读数为 JSON payload(无固定列),SQLite json_extract 在 DB 端做 AVG/GROUP BY(被 (device_id, recorded_at) 索引圈住)。
  • FK ON DELETE RESTRICT:有读数的设备拒删,引导改为 enabled=false。
  • 设备 uuid(uuid4 内部生成)是稳定身份锚点:API 路径键、HA Discovery unique_id 来源,不随 friendly_name 改变。

第二条 MQTT 通道(HA Discovery 发布)

与已有 REST 通道(app/integrations/homeassistant.py、POST /homeassistant/publish)并行、不冲突:

MQTT 通道(M5 新增):
  paho-mqtt MqttManager(lifespan 长连接,配置变更可重连)
        │
        ├─► Discovery config(retained)
        │     topic: <prefix>/<component>/<node>/<object>/config
        │     内容:device 块(identifiers=uuid)、state_topic、unique_id(uuid+key)、
        │           name(friendly_name)、device_class、unit_of_measurement、availability
        │     时机:连接成功时 / 目录或勾选变更时(全量重发);
        │           取消勾选时发空 payload(清除 entity)
        │
        └─► State / Availability(非 retained)
              时机:每次轮询成功后推该设备所有 enabled entity 的最新值;
                    周期兜底 job 重推所有 enabled entity + online topic

expose 框架:
  provider 动态产出 ExposableEntity 目录(元数据从 YAML profile 派生)
  ExposedEntityToggle 表:只存逐 key 开关(default=disabled)
  build_catalog(session) → 目录 + 勾选状态(合并所有 provider)

HA entity 身份模型(Z2M 语义):

  • unique_id = f"{device.uuid}_{metric.key}"(稳定,改名不变)
  • name = friendly_name(改名重发 discovery,HA 显示名跟着变、历史不丢)
  • 每设备除各 sensor entity 外,另有 binary_sensor online(取 last_poll_ok)——这是"不止 sensor"的体现

当前约束

  • 当前数据库继续使用 SQLite
  • 当前不引入前后端分离 已退役(M2):现为 React SPA + JSON /api 层,由 FastAPI 同源托管
  • 当前不设计 Notion 模块
  • 当前通知能力仍保持极小范围,不引入独立通知中心或多渠道抽象
  • Modbus 当前仅 TCP(Waveshare RTU↔TCP 网关),只读(FC03/04),不写设备寄存器

关于 Notion

Notion 在 Go 版本中仍是现状模块,但在 Python 重构中已经明确属于 removed scope。

因此当前 Python skeleton:

  • 不提供 Notion integration 模块
  • 不提供 Notion schema
  • 不预留 Notion 相关业务流

如果未来需要回顾其历史作用,应继续参考 Go 版本和现有迁移盘点文档,而不是在 Python 骨架中保留它。