fastapi sse websocket 智能家居实时控制台

fastapi sse websocket 智能家居实时控制台

本文档以"智能家居控制台"项目为载体,按章节讲解 FastAPI + SSE + WebSocket 的完整开发流程。

项目后端文件为 main.py,前端为 index.html,使用 uv 管理依赖。


目录

  1. 项目概述与架构
  2. [环境搭建与 uv 依赖管理](#环境搭建与 uv 依赖管理)
  3. 后端:设备状态模型
  4. 后端:设备管理器与并发控制
  5. [后端:HTTP 路由与静态文件](#后端:HTTP 路由与静态文件)
  6. [后端:SSE 传感器数据流](#后端:SSE 传感器数据流)
  7. [后端:WebSocket 控制通道](#后端:WebSocket 控制通道)
  8. [前端:页面结构与 CSS 设计系统](#前端:页面结构与 CSS 设计系统)
  9. [前端:SSE 客户端实现](#前端:SSE 客户端实现)
  10. [前端:WebSocket 客户端实现](#前端:WebSocket 客户端实现)
  11. 前端:实时数据渲染与交互
  12. 部署与运行

第 1 章 · 项目概述与架构

1.1 项目目标

构建一个 IoT 场景的智能家居控制台,实现:

  • SSE(Server-Sent Events) 单向推送传感器数据:温度、湿度、门锁状态。
  • WebSocket 双向通道下发控制命令:开灯、关灯、调温。
  • 虚拟设备 / 硬件演示 两种数据源切换,模拟硬件/虚拟设备切换。

1.2 技术栈

层级 技术 说明
后端框架 FastAPI 0.141+ 现代 Python 异步 Web 框架
ASGI 服务器 uvicornstandard 高性能异步服务器,含 uvloop 加速
实时通信 SSE + WebSocket SSE 推数据,WS 发命令
前端 原生 HTML/CSS/JS 无构建依赖,单文件部署
包管理 uv Rust 编写的极速 Python 包管理器

1.3 架构图

1.4 数据流

传感器数据流(SSE,每秒一次):

复制代码
后端 DeviceHub.read_both()
  → 生成 {virtual: {...}, hardware: {...}} 双模快照
  → SSE 事件帧: event: telemetry\ndata: {...}\n\n
  → 前端 fetch ReadableStream 接收
  → renderTelemetry() 按当前模式过滤渲染

控制命令流(WebSocket,按需):

复制代码
前端 sendCommand("开灯")
  → WS 发送: {"command":"开灯","mode":"virtual"}
  → 后端 hub.command() 执行
  → WS 返回: {"type":"ack","command":"开灯","state":{...}}
  → 前端 renderTelemetry() 更新 UI

第 2 章 · 环境搭建与 uv 依赖管理

2.1 为什么用 uv

uv 是 Astral 公司用 Rust 开发的 Python 包管理器,特点:

  • 速度极快:比 pip 快 10-100 倍。
  • 兼容 pipuv pip installpip install 语法一致。
  • 项目管理uv init + uv add 类似 npm 的流程,自动管理 pyproject.toml 和虚拟环境。

2.2 初始化项目

bash 复制代码
# 进入项目目录
cd fastapi-sse-ws

# 初始化 uv 项目(生成 pyproject.toml)
uv init --no-readme --no-pin-python

2.3 安装依赖

bash 复制代码
# 安装 FastAPI 和 uvicorn(不指定版本,自动获取最新)
uv add fastapi "uvicorn[standard]"

uv add 会自动:

  1. 创建 .venv 虚拟环境(如果不存在)。
  2. 解析依赖树并安装到虚拟环境。
  3. 更新 pyproject.tomldependencies 字段。
  4. 生成 uv.lock 锁定文件。

2.4 pyproject.toml 结构

toml 复制代码
[project]
name = "fastapi-sse-ws"
version = "0.1.0"
description = "智能家居实时控制台 --- SSE 推传感器数据, WS 发控制命令"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.141.1",
    "uvicorn[standard]>=0.52.1",
]
  • requires-python:最低 Python 版本要求。
  • dependencies:生产依赖列表,uv add 自动维护。
  • "uvicorn[standard]":安装 uvicorn 及其可选依赖(uvloop、httptools 等)。

2.5 常用 uv 命令

命令 作用
uv init 初始化新项目
uv add <包名> 添加依赖
uv add --dev <包名> 添加开发依赖
uv remove <包名> 移除依赖
uv sync 按 lock 文件同步安装依赖
uv run <命令> 在项目虚拟环境中执行命令
uv lock 更新锁文件

第 3 章 · 后端:设备状态模型

对应代码:main.py 第 1 章 · DeviceState

3.1 dataclass 简介

Python 的 @dataclass 装饰器自动生成 __init____repr__ 等方法,适合定义纯数据容器:

python 复制代码
from dataclasses import dataclass

@dataclass
class DeviceState:
    mode: str
    temperature: float
    humidity: float
    target_temperature: float = 24.0  # 带默认值的字段
    light_on: bool = False
    door_locked: bool = True
    sequence: int = 0

3.2 传感器模拟逻辑(advance 方法)

advance() 方法每秒被调用一次,模拟传感器数据的自然漂移:

python 复制代码
def advance(self) -> None:
    self.sequence += 1
    # 虚拟设备:快速趋近 + 大噪声
    if self.mode == "virtual":
        self.temperature += (self.target - self.temperature) * 0.045
        self.temperature += random.uniform(-0.10, 0.10)
    # 硬件演示:缓慢趋近 + 小噪声
    else:
        self.temperature += (self.target - self.temperature) * 0.025
        self.temperature += random.uniform(-0.035, 0.035)
    # 钳制到合理范围
    self.temperature = min(30.0, max(16.0, self.temperature))

关键设计: 虚拟和硬件模式使用不同的参数(趋近系数、噪声幅度),模拟真实硬件传感器数据更平滑的特性。

3.3 状态快照(snapshot 方法)

snapshot() 将内部状态导出为 JSON 可序列化的字典,附加元信息(数据源 ID、延迟、信号强度等):

python 复制代码
def snapshot(self, active_streams: int = 0) -> dict[str, Any]:
    return {
        "sequence": self.sequence,
        "temperature": round(self.temperature, 1),
        "source_id": "SIM-HOME-01" if is_virtual else "GW-BUPT-01",
        "rssi": None if is_virtual else random.randint(-68, -55),
        # ... 其他字段
    }

第 4 章 · 后端:设备管理器与并发控制

对应代码:main.py 第 2 章 · DeviceHub

4.1 为什么需要锁

SSE 端点和 WebSocket 端点可能同时读写设备状态。asyncio.Lock 确保同一时刻只有一个协程操作状态:

python 复制代码
class DeviceHub:
    def __init__(self) -> None:
        self.states = {"virtual": DeviceState(...), "hardware": DeviceState(...)}
        self._lock = asyncio.Lock()

    async def read_both(self) -> dict[str, dict[str, Any]]:
        async with self._lock:  # 获取锁
            for state in self.states.values():
                state.advance()
            return {mode: state.snapshot() for ...}

4.2 双模快照设计

SSE 端点每帧推送两种模式的数据,而非按 query 参数过滤:

python 复制代码
async def read_both(self):
    # 返回 {"virtual": {...}, "hardware": {...}}

为什么这样设计? 前端切换模式时只需改变 appState.mode,SSE 连接保持不变,避免 EventSource.close() / fetch.abort() 产生的 net::ERR_ABORTED 控制台日志。

4.3 命令执行(command 方法)

python 复制代码
async def command(self, mode, command, payload):
    if mode == "hardware":
        await asyncio.sleep(0.12)  # 模拟硬件链路延迟
    async with self._lock:
        state = self.states[mode]
        if command == "开灯":
            state.light_on = True
        elif command == "调温":
            target = float(payload["target"])
            if not 16 <= target <= 30:
                raise ValueError("目标温度须在 16--30°C 之间")
            state.target_temperature = round(target, 1)
        return {"type": "ack", "state": state.snapshot()}

第 5 章 · 后端:HTTP 路由与静态文件

对应代码:main.py 第 3 章

5.1 路由装饰器

FastAPI 使用装饰器声明路由:

python 复制代码
@app.get("/", include_in_schema=False)      # 不出现在 /docs 中
async def index() -> FileResponse:
    return FileResponse(BASE_DIR / "index.html")

@app.get("/api/health")
async def health() -> dict[str, Any]:
    return {"status": "ok", "sse_clients": hub.active_streams}

5.2 FastAPI 最新语法要点

  • 异步优先 :所有路由使用 async def,FastAPI 0.100+ 推荐 async-first。
  • 类型提示 :返回类型注解(如 -> dict[str, Any])自动生成 OpenAPI schema。
  • Pydantic V2:FastAPI 0.128+ 移除了 Pydantic V1 支持,当前使用 Pydantic V2。
  • lifespan@app.on_event("startup") 已废弃,推荐使用 lifespan 上下文管理器(本项目无全局资源需管理,故未使用)。

第 6 章 · 后端:SSE 传感器数据流

对应代码:main.py 第 4 章 · sensor_events

6.1 SSE 协议简介

Server-Sent Events 是 HTTP 长连接上的单向推送协议。报文格式:

复制代码
event: telemetry\n
data: {"temperature": 24.5}\n
\n

每个事件以 \n\n 结尾。浏览器通过 EventSourcefetch + ReadableStream 接收。

6.2 StreamingResponse

FastAPI 的 StreamingResponse 接受一个异步生成器,逐块发送响应体:

python 复制代码
@app.get("/api/events")
async def sensor_events(request: Request) -> StreamingResponse:
    async def event_stream():
        hub.active_streams += 1
        try:
            while True:
                if await request.is_disconnected():
                    break
                payload = await hub.read_both()
                yield f"event: telemetry\ndata: {json.dumps(payload)}\n\n"
                await asyncio.sleep(1.0)
        finally:
            hub.active_streams = max(0, hub.active_streams - 1)

    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
    )

6.3 关键响应头

头部 作用
Cache-Control: no-cache 禁用代理缓存
Connection: keep-alive 保持长连接
X-Accel-Buffering: no 禁止 Nginx 缓冲(确保实时推送)

6.4 断开检测

request.is_disconnected() 是 Starlette 提供的异步方法,检测客户端是否已断开。在 finally 中递减计数器,确保异常退出也能正确清理。


第 7 章 · 后端:WebSocket 控制通道

对应代码:main.py 第 5 章 · control_socket

7.1 WebSocket 端点定义

python 复制代码
@app.websocket("/ws/control")
async def control_socket(websocket: WebSocket) -> None:
    await websocket.accept()  # 完成握手
    await websocket.send_json({"type": "connected", ...})
    while True:
        try:
            data = await websocket.receive_json()
            response = await hub.command(data["mode"], data["command"], data)
            await websocket.send_json(response)
        except WebSocketDisconnect:
            break

7.2 消息协议

方向 类型 格式
客户端→服务端 命令 {"command":"开灯","mode":"virtual"}
服务端→客户端 连接确认 {"type":"connected","message":"..."}
服务端→客户端 命令回执 {"type":"ack","command":"开灯","state":{...}}
服务端→客户端 错误 {"type":"error","message":"..."}

7.3 错误处理

python 复制代码
except WebSocketDisconnect:    # 客户端正常断开
    break
except (json.JSONDecodeError, TypeError):  # 无效 JSON
    await websocket.send_json({"type": "error", "message": "命令必须是有效 JSON"})

第 8 章 · 前端:页面结构与 CSS 设计系统

对应代码:index.html 第 1-15 章 CSS

8.1 设计令牌

使用 CSS 自定义属性集中管理颜色、字体:

css 复制代码
:root {
  --bg: #090b0c;
  --acid: #b6f36b;      /* 强调色 */
  --font-display: "Bahnschrift", sans-serif;
}

8.2 三栏布局

css 复制代码
.console {
  display: grid;
  grid-template-columns: 290px minmax(520px, 1fr) 330px;
}
  • 左栏:传感器数据(温度曲线、湿度、门锁、数据源信息)
  • 中栏:住宅数字孪生平面图(CSS Grid 绘制的三房间)
  • 右栏:控制终端(灯光开关、温控步进、命令日志)

8.3 响应式断点

断点 布局变化
≤1120px 缩小列宽,隐藏连接状态
≤900px 平面图占满上方,左右面板下方并排
≤620px 单列堆叠

第 9 章 · 前端:SSE 客户端实现

对应代码:index.html JS 第 6 节 · connectSSE

9.1 为什么不用 EventSource

EventSource.close() 会在 Chrome 控制台产生 net::ERR_ABORTED 网络日志。改用 fetch + ReadableStream 可以干净地管理连接:

javascript 复制代码
fetch('/api/events')
  .then(response => {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    const pump = () => reader.read().then(({ done, value }) => {
      if (done) return;
      buffer += decoder.decode(value, { stream: true });
      // 按 \n\n 分割 SSE 事件
      let boundary;
      while ((boundary = buffer.indexOf('\n\n')) !== -1) {
        const chunk = buffer.slice(0, boundary);
        buffer = buffer.slice(boundary + 2);
        chunk.split('\n').forEach(line => {
          if (line.startsWith('data:')) {
            renderTelemetry(JSON.parse(line.slice(5).trim()));
          }
        });
      }
      return pump();
    });
    return pump();
  });

9.2 双模数据过滤

后端每帧推送 {virtual: {...}, hardware: {...}},前端按当前模式取值:

javascript 复制代码
function renderTelemetry(data) {
  const target = data[appState.mode];  // 只取当前模式的数据
  if (!target) return;
  // 更新 UI...
}

切换模式时只需 appState.mode = 'hardware',SSE 连接保持不变。


第 10 章 · 前端:WebSocket 客户端实现

对应代码:index.html JS 第 8-10 节

10.1 连接管理

javascript 复制代码
function connectWebSocket() {
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
  const socket = new WebSocket(`${protocol}//${location.host}/ws/control`);
  socket.onopen = () => setChannel('ws', true, '控制链在线');
  socket.onmessage = event => { /* 处理 ACK / error */ };
  socket.onclose = () => setTimeout(connectWebSocket, 1600);  // 自动重连
}

10.2 命令发送

javascript 复制代码
function sendCommand(command, payload = {}) {
  if (!appState.socket || appState.socket.readyState !== WebSocket.OPEN) return;
  appState.socket.send(JSON.stringify({ command, mode: appState.mode, ...payload }));
}

10.3 ACK 处理

收到 ACK 后,如果回执的设备模式与当前选中模式一致,则更新 UI:

javascript 复制代码
if (data.type === 'ack') {
  if (data.mode === appState.mode) renderTelemetry({ [data.mode]: data.state });
}

第 11 章 · 前端:实时数据渲染与交互

对应代码:index.html JS 第 4-5 节

11.1 温度折线图

使用原生 SVG <path> 绘制最近 24 个采样点:

javascript 复制代码
function renderChart(value) {
  appState.temperatures.push(value);
  if (appState.temperatures.length > 24) appState.temperatures.shift();
  // 将温度映射为 SVG 坐标 → 生成 path 字符串 → 更新 DOM
}

11.2 数值闪烁反馈

每次温度更新时触发 CSS 动画,提供视觉反馈:

javascript 复制代码
function pulse(element) {
  element.classList.remove('bump');
  void element.offsetWidth;  // 强制重排,重置动画
  element.classList.add('bump');
}

11.3 平面图灯光联动

通过 CSS 类切换实现灯光效果:

javascript 复制代码
ui.floorplan.classList.toggle('light-on', target.light_on);

.light-on 类会触发光晕放大、灯具高亮、家具提亮等一系列 CSS 过渡动画。


第 12 章 · 部署与运行

12.1 开发模式

bash 复制代码
# 安装依赖
uv sync

# 启动开发服务器(支持热重载)
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000

12.2 生产模式

bash 复制代码
# 多 worker 启动
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

注意: 多 worker 模式下,每个 worker 进程持有独立的 DeviceHub 实例,状态不共享。生产环境如需共享状态,需引入 Redis Pub/Sub。

12.3 Nginx 反向代理

nginx 复制代码
location /api/events {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # SSE 必须关闭缓冲
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
}

location /ws/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

12.4 验证

打开浏览器访问 http://127.0.0.1:8000

  1. SSE 指示灯变绿,温度每秒更新。
  2. WS 指示灯变绿,日志显示"控制通道已建立"。
  3. 点击灯光开关,平面图灯亮,日志出现 ACK 001
  4. 切换"硬件演示",数据源 ID 变为 GW-BUPT-01,出现 RSSI 信号值。

相关推荐
杰佛史彦明 本王是暴君1 小时前
PyTorch KernelAgent 源码解读 ---(2)--- 总体流程
人工智能·pytorch·python
Zane19941 小时前
别再手写 try/finally 了:一文讲透 with 语句背后的上下文管理器协议
后端·python
李可以量化2 小时前
量化高性能服务框架 Tornado 全面解析(上):异步非阻塞的核心能力与场景落地
大数据·python·量化交易·tornado·qmt·ptrade
内蒙深海大鲨鱼2 小时前
3.Introduction to PyTorch YouTube Series--Autograd
人工智能·pytorch·python
用户0332126663672 小时前
使用 Python 在 Excel 中添加或删除批注
python·excel
dogstarhuang2 小时前
手把手用 Doubao-Seed-Evolving 写一个网站监控脚本:完整代码与两处踩坑
python·ai编程·掘金技术征文
ZZHow10242 小时前
PyTorch深度学习入门笔记(小土堆)P7-14
人工智能·pytorch·笔记·python·深度学习
花生了什么事o2 小时前
WebSocket 与 SSE:实时通信方案的选择
网络·websocket·网络协议
很楠爱上3 小时前
AI项目------赛博负熵:拆解一个 Codex 生成的英语背单词全栈工程(附源码文件免费)
人工智能·python·codex