提交 ff749e33 authored 作者: 陈泽健's avatar 陈泽健

feat(device-sim): 门口屏模拟器启动时调用 getTokenInfoByToken 获取 token

新增 DoorTokenClient(固定 X-SIGN + regUdid 派生 + 响应解析),
TokenPool 登记池保证批量场景 appToken 不重复(来自导入授权码,不可自动生成);
DoorSimulator.start() 接入 token 获取并回填 topic_params(company_id/room_id/conference);
base_simulator / create_simulator / start_simulator 透传 token_api_host(来源 EnvConfig)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 0e4ead5b
# 执行计划 — 门口屏模拟器 Token 获取
> **文档类型**: 执行计划文档
> **创建日期**: 2026-08-19
> **作者**: czj
> **关联文档**: `_PRD_需求优化_门口屏模拟器Token获取.md`、`_PRD_需求文档_设备模拟模块.md`
> **预计工期**: 2.5 天
---
## 一、执行概述
### 1.1 项目背景
真实门口屏设备(UbGview 应用)启动后第一个动作是调用 HTTP 接口 `POST /exapi/api-android/token/getTokenInfoByToken` 获取 token。本计划为门口屏模拟器补充该接口调用链,使模拟器启动流程与真实设备一致——先获取 token,为后续 MQTT 主题解析和上报做准备。
### 1.2 执行目标
1. 门口屏模拟器启动时第一步调用 `getTokenInfoByToken` 接口
2. 使用日志中的固定 X-SIGN(已验证可调用成功)
3. 多设备场景下 regUdid 由 device_id 派生,保证唯一
4. 成功获取并保存 Authorization / token / conference / company 信息
5. 将 companyNumber / cnum 回填到 topic_params
### 1.3 范围界定
| 范围 | 内容 |
|------|------|
| ✅ 本期执行 | HTTP 接口调用、签名头构造、regUdid 生成、响应解析、token 保存、参数回填 |
| ❌ 本期排除 | MQTT 主题解析、会议服务主题订阅、MQTT 周期上报、动态 X-SIGN 算法 |
---
## 二、任务分解与实施计划
### 阶段 1:接口调研与数据确认(0.5 天)
| # | 任务 | 说明 | 产出 |
|---|------|------|------|
| 1.1 | 确认接口调用细节 | 复核日志中的 URL / Header / Body / Response 结构 | 接口调用手顺(已确认:HTTP 200 + OPERATION_SUCCESSFUL) |
| 1.2 | 确认目标环境地址 | 从 EnvConfig 中获取目标系统 host(如 192.168.5.48) | host 配置项 |
| 1.3 | 确认 X-SIGN 固定值 | 从日志中提取固定 X-SIGN 作为常量 | 常量定义 |
| 1.4 | 确认 appToken 固定值 | 从日志 body 中提取 `AND-34H-0101` | 常量定义 |
**阶段 1 完成标准**:接口调用所需的最小常量集合(host / X-SIGN / appToken)已确认并记录。
### 阶段 2:Token 客户端实现(1 天)
| # | 任务 | 说明 | 产出 |
|---|------|------|------|
| 2.1 | 新增 `DoorTokenClient` 类 | 文件:`backend/app/simulators/door_token_client.py` | 客户端类 |
| 2.2 | 实现 `_build_headers()` | X-SIGN 固定值 + X-TIMESTAMP(秒级)+ X-RANDOM(16 位) | 请求头构造 |
| 2.3 | 实现 `_generate_reg_udid()` | `{device_id}_{8位随机}`,保证多设备唯一 | regUdid 生成 |
| 2.4 | 实现 `get_token()` | POST 调用 + 超时控制(5s)+ 重试(3 次, 指数退避) | 接口调用 |
| 2.5 | 实现 `_parse_response()` | 解析 Authorization / token / conference / company | 响应解析 |
| 2.6 | 实现 `_get_mqtt_config()`(预留) | 解析日志中的 mqttSavedConfig(本期仅预留,不做订阅) | 预留方法 |
**阶段 2 完成标准**`DoorTokenClient` 可独立调用接口成功,单元验证返回完整 token 数据。
### 阶段 3:模拟器启动流程集成(0.5 天)
| # | 任务 | 说明 | 产出 |
|---|------|------|------|
| 3.1 | `DoorSimulator.start()` 接入 token 获取 | 在 start() 最前面调用 `DoorTokenClient.get_token()` | 启动流程改造 |
| 3.2 | 保存 token 信息 | 新增 `self._token_info` 属性保存完整响应 | token 保存 |
| 3.3 | topic_params 回填 | company_id ← companyNumber, room_id ← cnum, app_token ← appToken | 参数回填 |
| 3.4 | 失败处理 | token 获取失败 → 记录错误日志 → 启动终止 → 设备状态 error | 异常处理 |
**阶段 3 完成标准**:启动门口屏设备,日志显示接口调用成功,token 已保存,topic_params 已回填。
### 阶段 4:验证与部署(0.5 天)
| # | 任务 | 说明 | 产出 |
|---|------|------|------|
| 4.1 | 单设备验证 | 创建 1 台门口屏设备,启动,确认 token 获取成功 | 验证记录 |
| 4.2 | 多设备验证 | 创建 3-5 台门口屏设备,启动,确认 regUdid 均不重复 | 验证记录 |
| 4.3 | 失败场景验证 | 模拟网络不可达,确认重试 3 次后启动终止 | 验证记录 |
| 4.4 | 回归验证 | 其他设备类型(paperless/central)启动不受影响 | 回归记录 |
| 4.5 | 部署 | 提交代码,部署到测试服务器(192.168.5.60 双容器) | 部署完成 |
**阶段 4 完成标准**:单设备 + 多设备 + 失败场景全部验证通过,线上部署完成。
---
## 三、验收标准
### 3.1 功能验收
| 编号 | 验收项 | 预期结果 |
|------|--------|---------|
| F01 | 门口屏启动时调用 token 接口 | 日志显示 `POST /exapi/api-android/token/getTokenInfoByToken` 记录 |
| F02 | 接口调用成功 | `code=200, success=true`,Authorization 已保存 |
| F03 | token 信息保存 | `_token_info` 包含 Authorization / token / conference / company |
| F04 | topic_params 回填 | company_id / room_id 自动回填为接口返回值 |
| F05 | regUdid 唯一性 | 多设备启动时 regUdid 各不相同 |
| F06 | 接口调用失败 | 启动终止,日志显示错误原因,设备状态 error |
### 3.2 边界验收
| 编号 | 边界场景 | 预期结果 |
|------|---------|---------|
| B01 | 网络不可达 | 3 次重试后失败,启动终止 |
| B02 | 接口返回 success: false | 立即失败,不重试 |
| B03 | 响应缺少 Authorization | 解析异常,启动终止 |
| B04 | 单设备启动 | regUdid 正常生成,token 获取成功 |
| B05 | 多设备(10 台)同时启动 | 各设备独立获取 token,regUdid 均不重复 |
---
## 四、测试计划
| 测试类型 | 范围 | 方式 |
|---------|------|------|
| 单元测试 | `DoorTokenClient` 的 header/body/regUdid/解析逻辑 | pytest,mock 网络 |
| 集成测试 | 真实调用目标环境接口 | 手工验证(已验证 HTTP 200 可调用成功) |
| 手动验证 | 单设备 / 多设备 / 失败场景 | 前端界面操作 + 后端日志 |
---
## 五、风险评估
| 风险 | 等级 | 影响 | 应对措施 |
|------|------|------|---------|
| 固定 X-SIGN 后续失效 | 中 | 接口调用 401 | 短期限固定值,后续逆向 APK 生成动态签名 |
| X-SIGN 与时间戳校验 | 中 | 当前时间戳可能导致签名失效 | 已实际调用验证成功,若失效回退用日志时间戳 |
| 服务端 regUdid 唯一校验严格 | 低 | 多设备启动部分失败 | regUdid 由 device_id 派生 + 随机后缀,冲突概率极低 |
| 目标环境网络不可达 | 中 | 启动失败 | 重试 3 次 + 明确错误日志 |
| 多设备并发调用目标环境 | 中 | 目标环境压力 | 每设备独立请求,接口轻量,风险可控 |
---
## 六、实施记录
| 日期 | 任务 | 状态 | 备注 |
|------|------|------|------|
| 2026-08-19 | 梳理需求,确认决策 | ✅ 完成 | 与用户确认 X-SIGN 固定值、regUdid 派生、只存 token |
| 2026-08-19 | 接口实际调用验证 | ✅ 完成 | HTTP 200 + OPERATION_SUCCESSFUL,token 完整返回 |
| 2026-08-19 | 撰写 PRD 需求文档 | ✅ 完成 | `_PRD_需求优化_门口屏模拟器Token获取.md` |
| 2026-08-19 | 撰写执行计划 | ⏳ 进行中 | 本文档 |
| - | 阶段 1 接口调研与数据确认 | ⬜ 待执行 | |
| - | 阶段 2 Token 客户端实现 | ⬜ 待执行 | |
| - | 阶段 3 模拟器启动流程集成 | ⬜ 待执行 | |
| - | 阶段 4 验证与部署 | ⬜ 待执行 | |
---
## 七、后续工作(不在本期)
| 后续项 | 说明 | 优先级 |
|--------|------|--------|
| MQTT 主题解析 | 根据回填后的 company_id + room_id 解析 MQTT 主题 | P1 |
| 会议服务主题订阅 | 订阅 `/iot/v1/conference/service/request/{company_id}/{room_id}/` | P1 |
| MQTT 周期上报 | 启动周期上报任务 | P1 |
| 动态 X-SIGN 算法 | 逆向 APK 确认签名算法,替换固定值 | P2 |
| 其他设备类型接入 | 无纸化屏等设备复用 TokenClient | P2 |
---
## 八、附录
### 8.1 涉及代码文件
| 文件 | 操作 | 说明 |
|------|------|------|
| `backend/app/simulators/door_token_client.py` | 新增 | Token 获取客户端 |
| `backend/app/simulators/door_simulator.py` | 修改 | start() 接入 token 获取 |
| `backend/app/config.py` | 修改(可选) | token 接口相关配置项 |
### 8.2 接口调用手顺(已验证)
```
POST https://192.168.5.48/exapi/api-android/token/getTokenInfoByToken
Headers:
Content-Type: application/json; charset=utf-8
X-SIGN: {日志固定值}
X-TIMESTAMP: {当前秒级时间戳}
X-RANDOM: {16 位随机}
Body:
{"appToken": "AND-34H-0101", "regUdid": "{device_id}_{8位随机}"}
Response:
code=200, success=true
result.Authorization → 保存
result.token.companyNumber → topic_params.company_id
result.token.cnum → topic_params.room_id
```
---
*文档结束*
\ No newline at end of file
...@@ -9,6 +9,71 @@ ...@@ -9,6 +9,71 @@
## 📊 会话进度记录 ## 📊 会话进度记录
### 2026-08-19 会话 E:门口屏模拟器 Token 获取(已实现 + 真实调用验证通过,未提交未部署)
**会话目标**:为门口屏(door)模拟器补充启动时的真实 HTTP 接口调用链 —— 模拟真实 Android 门口屏设备(UbGview 应用)启动第一步调用的 `POST /exapi/api-android/token/getTokenInfoByToken` 接口获取 token。
**状态**:✅ 代码实现完成 + 真实接口调用验证通过(HTTP 200 + 完整 token)+ 登记池 5 场景验证通过;⚠️ 未提交 git、未部署
**背景**:真实门口屏设备启动第一步是调 token 接口,成功后才有 Authorization 继续进行后续交互;而模拟器之前是直接连 MQTT,且 company_id / room_id 依赖手动配置。
**本期只做**:启动时调用 token 接口 → 保存 token → 回填 topic_params。
**明确不做**(后续迭代):MQTT 主题解析、会议服务主题订阅、MQTT 周期上报、动态 X-SIGN 算法。
---
#### ① 新增 Token 客户端(`backend/app/simulators/door_token_client.py`)
- **接口**`POST {scheme}://{host}/exapi/api-android/token/getTokenInfoByToken`(默认 https;host 自动识别协议,兼容 `https://` / `http://` / `host:port` 等写法)
- **请求头**:X-SIGN 用日志固定值 `X_SIGN_FIXED`(动态算法未复现,实测可调用成功);X-TIMESTAMP 秒级;X-RANDOM 16 位随机
- **请求体**`{"appToken": ..., "regUdid": ...}`
- **regUdid**:由 device_id 确定性派生 `reg_{md5(device_id)[:16]}` —— 批量不重复、同设备多次启动结果一致(格式与真实日志 16 位 hex 一致)
- **appToken 登记池**(用户明确的关键决策):appToken **不自动生成**,来自设备导入时提供的授权码。`TokenPool.allocate()` 只做登记与去重:
- 未提供 → 报错"门口屏 token 获取必须使用导入设备时提供的服务端授权码,不可自动生成"
- 重复使用 → 报错"appToken 已被其他设备使用,批量场景授权码不能重复"
- **响应解析(勘误要点)**`code`**字符串 "200"**`conference` / `company` **嵌套在 `result.token` 对象内部**(不在 result 顶层)
- **重试**:网络错误(ConnectionError/Timeout)指数退避重试 3 次;业务错误(ValueError)不重试
- **host 来源**`EnvConfig.default_topic_params['token_api_host']`,回退 `broker_host`
#### ② 启动流程集成(4 个后端文件)
- `DoorSimulator.start()` 改造为三步:
1. 调 token 接口(登记 appToken → `DoorTokenClient.get_token()` → 保存 `self._token_info`
2. 回填 `topic_params`:companyNumber→company_id、cnum→room_id、conferenceId→conference_id、conferenceName→conference_name、appToken→app_token
3. 重新解析主题(`_resolve_topics()`)后走父类 MQTT 启动流程
- token 获取失败 → 记录错误日志 → `_notify_report` 上报 `token_get` failed 消息 → 启动终止返回 False
- **未配置 `token_api_host` → 仅 warning 跳过 token 获取,不影响启动(向后兼容,非门口屏不受影响)**
#### 验证结果
| 验证项 | 结果 |
|--------|------|
| 真实调用(appToken=AND-34H-0101 + regUdid) | ✅ HTTP 200 + OPERATION_SUCCESSFUL,token 完整返回 |
| 字段回填 | ✅ companyNumber=CN-34H-UBAINS、cnum=upjp7gzk..., conferenceName=PGY测试会议室1 |
| 未提供 appToken | ✅ 明确报错拒绝自动生成 |
| 重复使用同一授权码 | ✅ 拒绝 |
| 不同授权码 | ✅ 正常登记 |
| regUdid 派生唯一性 | ✅ `reg_{md5(device_id)[:16]}` 确定性且唯一 |
**修改文件清单(本次会话)**
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 新增 | DoorTokenClient + TokenPool 登记池 + regUdid 派生 + host 规范化 + 默认 host 选择 |
| `backend/app/simulators/door_simulator.py` | 修改 | start() 接入 token 获取 + `_token_info` + topic_params 回填 |
| `backend/app/simulators/base_simulator.py` | 修改 | `__init__` 增加 `token_api_host` 参数 |
| `backend/app/simulators/__init__.py` | 修改 | `create_simulator()` 透传 `token_api_host` |
| `backend/app/services/device_sim_service.py` | 修改 | `start_simulator()` 从环境配置读取 token_api_host |
| `Docs/PRD/需求文档/设备模拟/_PRD_需求优化_门口屏模拟器Token获取.md` | 新增 | PRD 需求文档 |
| `Docs/PRD/需求文档/设备模拟/_执行计划_门口屏模拟器Token获取.md` | 新增 | 执行计划文档 |
**待办**
- ⚠️ 本次改动未提交 git(连同此前会话 B/C/D 未提交的设备模拟改动一起),下次会话 `/GitCommit` 提交
- ⚠️ 未部署。需要门口屏环境配置在 `EnvConfig.default_topic_params` 里配 `token_api_host`(如 `https://192.168.5.48`)才会触发 token 获取;批量创建门口屏时 Excel 必须提供"授权码(app_token)"列(每台唯一)
- 后续迭代:动态 X-SIGN 算法(P2)、MQTT 主题解析/订阅/上报(P1)见 `_执行计划_门口屏模拟器Token获取.md` 七、后续工作
---
### 2026-08-19 会话 D:无纸化授权码 + 环境配置弹窗简化 + 批量删除性能修复(已全部完成并部署) ### 2026-08-19 会话 D:无纸化授权码 + 环境配置弹窗简化 + 批量删除性能修复(已全部完成并部署)
**会话目标**:三项设备模拟模块优化 — ①无纸化导入模板加授权码列 ②环境配置弹窗简化 ③100 条设备批量删除卡"处理中"修复 **会话目标**:三项设备模拟模块优化 — ①无纸化导入模板加授权码列 ②环境配置弹窗简化 ③100 条设备批量删除卡"处理中"修复
......
...@@ -797,80 +797,89 @@ class DeviceSimService: ...@@ -797,80 +797,89 @@ class DeviceSimService:
if not config: if not config:
raise ValueError(f"关联环境配置不存在: {simulator.env_config_id}") raise ValueError(f"关联环境配置不存在: {simulator.env_config_id}")
# 先检查是否已在运行(用短锁快速判读,避免 IO 操作占用锁)
with _running_simulators_lock: with _running_simulators_lock:
if device_id in _running_simulators and _running_simulators[device_id].is_running(): if device_id in _running_simulators and _running_simulators[device_id].is_running():
return True return True
# 确保 MQTT 已连接 # 确保 MQTT 已连接(IO 操作,放在锁外,避免批量启动时长锁竞争)
if not mqtt_manager.is_connected(config.id): if not mqtt_manager.is_connected(config.id):
password = "" password = ""
if config.password_encrypted: if config.password_encrypted:
password = _decrypt_password(config.password_encrypted) password = _decrypt_password(config.password_encrypted)
client_id = f"{config.client_id_prefix}_{simulator.device_id[:8]}" client_id = f"{config.client_id_prefix}_{simulator.device_id[:8]}"
connected = mqtt_manager.connect( connected = mqtt_manager.connect(
env_config_id=config.id,
host=config.broker_host,
port=config.broker_port,
username=config.username,
password=password,
client_id=client_id,
use_tls=config.use_tls,
)
if not connected:
raise ConnectionError("MQTT 连接失败,请检查环境配置")
# 更新环境配置状态
config.status = "connected"
config.last_connected_at = datetime.now()
# 创建并启动模拟器,传递多主题配置
# 兼容旧数据:topics 为空时,从 topic_prefix 自动生成默认映射
topics = config.topics
if not topics:
prefix = config.topic_prefix or "device"
topics = {
"door": f"{prefix}/door",
"paperless": f"{prefix}/paperless",
"central": f"{prefix}/central",
"client": f"{prefix}/client",
}
sim = create_simulator(
device_type=simulator.device_type,
device_id=simulator.device_id,
env_config_id=config.id, env_config_id=config.id,
mqtt_manager=mqtt_manager, host=config.broker_host,
report_config=simulator.report_config, port=config.broker_port,
topics=topics, username=config.username,
topic_params=simulator.topic_params, password=password,
client_id=client_id,
use_tls=config.use_tls,
) )
if not sim: if not connected:
raise ValueError(f"不支持的设备类型: {simulator.device_type}") raise ConnectionError("MQTT 连接失败,请检查环境配置")
# 设置上报回调(记录日志) # 更新环境配置状态
async def report_callback(**kwargs): config.status = "connected"
try: config.last_connected_at = datetime.now()
await self._create_report_log(**kwargs)
except Exception as e: # 创建并启动模拟器(IO 操作,放在锁外)
logger.error(f"记录上报日志异常: {e}") # 兼容旧数据:topics 为空时,从 topic_prefix 自动生成默认映射
topics = config.topics
if not topics:
prefix = config.topic_prefix or "device"
topics = {
"door": f"{prefix}/door",
"paperless": f"{prefix}/paperless",
"central": f"{prefix}/central",
"client": f"{prefix}/client",
}
# 获取 token_api_host(从环境配置的 default_topic_params 中)
token_api_host = None
if config.default_topic_params:
token_api_host = config.default_topic_params.get("token_api_host") or config.broker_host
sim = create_simulator(
device_type=simulator.device_type,
device_id=simulator.device_id,
env_config_id=config.id,
mqtt_manager=mqtt_manager,
report_config=simulator.report_config,
topics=topics,
topic_params=simulator.topic_params,
token_api_host=token_api_host,
)
if not sim:
raise ValueError(f"不支持的设备类型: {simulator.device_type}")
# 设置上报回调(记录日志)
async def report_callback(**kwargs):
try:
await self._create_report_log(**kwargs)
except Exception as e:
logger.error(f"记录上报日志异常: {e}")
sim.set_report_callback(lambda **kw: threading.Thread( sim.set_report_callback(lambda **kw: threading.Thread(
target=lambda: self._sync_report_log(kw) target=lambda: self._sync_report_log(kw)
).start()) ).start())
success = sim.start() success = sim.start()
if success: if success:
# 启动成功后,用短锁写入内存表
with _running_simulators_lock:
_running_simulators[device_id] = sim _running_simulators[device_id] = sim
simulator.status = "running" simulator.status = "running"
simulator.updated_at = datetime.now() simulator.updated_at = datetime.now()
try: try:
await self.db.flush() await self.db.flush()
except Exception as e: except Exception as e:
logger.warning(f"更新设备状态失败(模拟器已启动): {device_id}, error={e}") logger.warning(f"更新设备状态失败(模拟器已启动): {device_id}, error={e}")
# 不抛出异常,模拟器已在运行 # 不抛出异常,模拟器已在运行
return success return success
def _sync_report_log(self, kwargs: dict) -> None: def _sync_report_log(self, kwargs: dict) -> None:
""" """
......
...@@ -39,7 +39,8 @@ def create_simulator(device_type: str, device_id: str, ...@@ -39,7 +39,8 @@ def create_simulator(device_type: str, device_id: str,
env_config_id: str, mqtt_manager: MqttManager, env_config_id: str, mqtt_manager: MqttManager,
report_config: Optional[dict] = None, report_config: Optional[dict] = None,
topics: Optional[dict] = None, topics: Optional[dict] = None,
topic_params: Optional[dict] = None) -> Optional[BaseSimulator]: topic_params: Optional[dict] = None,
token_api_host: Optional[str] = None) -> Optional[BaseSimulator]:
""" """
模拟器工厂方法 模拟器工厂方法
...@@ -53,6 +54,7 @@ def create_simulator(device_type: str, device_id: str, ...@@ -53,6 +54,7 @@ def create_simulator(device_type: str, device_id: str,
report_config: 上报配置,如 {'interval': 30, 'enabled': True} report_config: 上报配置,如 {'interval': 30, 'enabled': True}
topics: 多主题前缀映射,key 为设备类型,value 为主题前缀 topics: 多主题前缀映射,key 为设备类型,value 为主题前缀
topic_params: 主题动态参数,如 {'room_id': 'A101', 'company_id': '001'} topic_params: 主题动态参数,如 {'room_id': 'A101', 'company_id': '001'}
token_api_host: Token 接口主机地址(门口屏设备启动时调用 getTokenInfoByToken 用)
Returns: Returns:
BaseSimulator: 模拟器实例,如果设备类型不支持则返回 None BaseSimulator: 模拟器实例,如果设备类型不支持则返回 None
...@@ -73,6 +75,7 @@ def create_simulator(device_type: str, device_id: str, ...@@ -73,6 +75,7 @@ def create_simulator(device_type: str, device_id: str,
report_config=report_config, report_config=report_config,
topics=topics or {}, topics=topics or {},
topic_params=topic_params, topic_params=topic_params,
token_api_host=token_api_host,
) )
......
...@@ -44,7 +44,8 @@ class BaseSimulator(ABC): ...@@ -44,7 +44,8 @@ class BaseSimulator(ABC):
device_type: str, mqtt_manager: MqttManager, device_type: str, mqtt_manager: MqttManager,
report_config: Optional[dict] = None, report_config: Optional[dict] = None,
topics: Optional[dict] = None, topics: Optional[dict] = None,
topic_params: Optional[dict] = None): topic_params: Optional[dict] = None,
token_api_host: Optional[str] = None):
""" """
初始化模拟设备 初始化模拟设备
...@@ -56,6 +57,7 @@ class BaseSimulator(ABC): ...@@ -56,6 +57,7 @@ class BaseSimulator(ABC):
report_config: 上报配置,如 {'interval': 30, 'enabled': True} report_config: 上报配置,如 {'interval': 30, 'enabled': True}
topics: 多主题前缀映射,key 为设备类型,value 为主题前缀 topics: 多主题前缀映射,key 为设备类型,value 为主题前缀
topic_params: 主题动态参数,如 {'room_id': 'A101', 'company_id': '001'} topic_params: 主题动态参数,如 {'room_id': 'A101', 'company_id': '001'}
token_api_host: Token 接口主机地址(设备启动时调用 getTokenInfoByToken 用)
""" """
self.device_id = device_id self.device_id = device_id
self.env_config_id = env_config_id self.env_config_id = env_config_id
...@@ -70,6 +72,7 @@ class BaseSimulator(ABC): ...@@ -70,6 +72,7 @@ class BaseSimulator(ABC):
self._lock = threading.Lock() self._lock = threading.Lock()
self.topics = topics or {} self.topics = topics or {}
self.topic_params = topic_params or {} self.topic_params = topic_params or {}
self.token_api_host = token_api_host
# 解析真实主题模板 # 解析真实主题模板
self._resolved_topics: dict = {} self._resolved_topics: dict = {}
......
...@@ -40,7 +40,8 @@ class DoorSimulator(BaseSimulator): ...@@ -40,7 +40,8 @@ class DoorSimulator(BaseSimulator):
def __init__(self, device_id: str, env_config_id: str, def __init__(self, device_id: str, env_config_id: str,
mqtt_manager: MqttManager, report_config: Optional[dict] = None, mqtt_manager: MqttManager, report_config: Optional[dict] = None,
topics: Optional[dict] = None, topics: Optional[dict] = None,
topic_params: Optional[dict] = None): topic_params: Optional[dict] = None,
token_api_host: Optional[str] = None):
""" """
初始化门口屏模拟器 初始化门口屏模拟器
...@@ -51,12 +52,14 @@ class DoorSimulator(BaseSimulator): ...@@ -51,12 +52,14 @@ class DoorSimulator(BaseSimulator):
report_config: 上报配置,如 {'interval': 30, 'enabled': True} report_config: 上报配置,如 {'interval': 30, 'enabled': True}
topics: 多主题前缀映射 topics: 多主题前缀映射
topic_params: 主题动态参数 topic_params: 主题动态参数
token_api_host: Token 接口主机地址(启动时调用 getTokenInfoByToken)
""" """
super().__init__(device_id, env_config_id, "door", mqtt_manager, super().__init__(device_id, env_config_id, "door", mqtt_manager,
report_config, topics, topic_params) report_config, topics, topic_params, token_api_host)
self._door_status = "closed" self._door_status = "closed"
self._battery_level = random.randint(60, 100) self._battery_level = random.randint(60, 100)
self._call_active = False self._call_active = False
self._token_info: Optional[dict] = None
def build_register_payload(self) -> dict: def build_register_payload(self) -> dict:
"""构建设备注册消息""" """构建设备注册消息"""
...@@ -88,6 +91,81 @@ class DoorSimulator(BaseSimulator): ...@@ -88,6 +91,81 @@ class DoorSimulator(BaseSimulator):
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S") "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
} }
def start(self) -> bool:
"""
启动门口屏模拟器
流程:
1. 调用 getTokenInfoByToken 接口获取 token
2. 保存 token 信息并回填 topic_params
3. 调用父类 start() 执行 MQTT 注册/订阅/上报
Returns:
bool: 是否启动成功
"""
# 第一步:获取 token
token_host = self.token_api_host
if not token_host:
logger.warning(f"门口屏未配置 token_api_host,跳过 token 获取: {self.device_id}")
else:
try:
from app.simulators.door_token_client import DoorTokenClient, allocate_app_token
# 登记用户提供的 appToken(Excel 导入或手动创建时传入,保证不重复)
app_token = allocate_app_token(self.device_id, self.topic_params.get("app_token"))
client = DoorTokenClient(
host=token_host,
device_id=self.device_id,
app_token=app_token,
)
token_info = client.get_token()
# 保存 token 完整信息
self._token_info = token_info
# 回填 topic_params
token_data = token_info.get("token", {})
company_number = token_data.get("companyNumber", "")
cnum = token_data.get("cnum", "")
conference = token_info.get("conference", {})
conference_id = str(conference.get("conferenceId", ""))
conference_name = conference.get("conferenceName", "")
if company_number:
self.topic_params["company_id"] = company_number
if cnum:
self.topic_params["room_id"] = cnum
if conference_id:
self.topic_params["conference_id"] = conference_id
if conference_name:
self.topic_params["conference_name"] = conference_name
self.topic_params["app_token"] = app_token
logger.info(
f"门口屏 token 获取成功: device_id={self.device_id}, "
f"company_id={company_number}, room_id={cnum}"
)
except Exception as e:
logger.error(f"门口屏 token 获取失败: device_id={self.device_id}, error={e}")
self._running = False
# 将错误信息通知回调
self._notify_report(
topic="token_get",
payload={"device_id": self.device_id, "error": str(e)},
direction="publish",
status="failed",
error=str(e),
)
return False
# 第二步:重新解析主题(topic_params 已回填,解析出新的 MQTT 主题)
self._resolve_topics()
# 第三步:调用父类 MQTT 启动流程
return super().start()
def _on_command(self, topic: str, payload: dict) -> None: def _on_command(self, topic: str, payload: dict) -> None:
""" """
处理平台下发的指令 处理平台下发的指令
......
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论