diff --git a/doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md b/doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md new file mode 100644 index 0000000..324c396 --- /dev/null +++ b/doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md @@ -0,0 +1,398 @@ +# IoT 设备实时点值显示 + 设备控制实施方案 + +> **编写时间**: 2026-07-24 +> **编写人**: 猫娘工程师 幽浮喵 +> **适用版本**: SecMPS 仓库大屏 (warehouse) + 管理端 (web.vite) + IntegrationGateway +> **目标**: 解决 MC4 IoT 设备(温度探头/湿度探头/空调控制器)在管理端和大屏两个前端的实时点值显示与控制问题 + +--- + +## 一、问题背景 + +### 1.1 当前痛点 +1. **管理端**:base_device 列表中点击 IoT 设备的"实时数据"按钮,弹窗表格**无任何数据显示**(之前现场反馈过) +2. **大屏端**:warehouse 地图点击 IoT 设备标签,**实时点值不显示**,空调控制器无法下发制冷/制热/关机命令 +3. **数据正确性**:MC4 设备点位序号 1 是自动添加的"在线点",真实数据从序号 2 开始;空调控制器有多个有效点位(5=湿度、6=温度、2=制冷发射、3=制热发射、4=关机发射) + +### 1.2 根因汇总 +| # | 根因 | 文件 | +|---|---|---| +| ① | `Mc4Adapter.GetRealtimeValuesAsync` 用 `JsonSerializer.Deserialize>(json)!` 反序列化实时点值响应,**没有传 `JsonOpts`**,MC4 返回的 `{code, msg, data:[...]}` 包装结构无法被 List 直接反序列化 | `IntegrationGateway.Adapters.MC4/Mc4Adapter.cs:154` | +| ② | `base_device.vue` 的 `fetchRealtime` 直接 `realtimeValues.value = await r.json()`,**未做字段名映射**(网关返回 PascalCase:PointIndex/Value/UpdateTime/Interval,前端 table 列名是 camelCase:pointIndex/value/updateTime/interval) | `web.vite/src/views/warehouse/device_manager/base_device.vue:164-170` | +| ③ | `RealtimeDataPanel.vue` 同样的问题,`values.value = await r.json()` 没做字段映射 | `web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue:25` | +| ④ | 仓库 `DeviceInfo.vue` 调用的后端接口是老的(之前基于 warehouse_devicepoint 表),**未切换到新的 `/api/base_device/getPageData` 接口** | `warehouse/src/view/DeviceInfo.vue` | +| ⑤ | 仓库空调控制按钮的下发参数 `sourceDeviceId` 与后端 `ControlRequest` 期望的 `deviceId` **字段名不一致** | `warehouse/src/view/DeviceInfo.vue:618` | +| ⑥ | MC4 设备点位索引语义**仅在主人本地知道**(index=2 是真实数据/探头,空调 index=5=湿度/6=温度/2=制冷/3=制热/4=关机),未在代码/文档中固化,新人接手成本高 | 全局 | + +--- + +## 二、设备分类与点位映射表 + +### 2.1 IoT 设备三分类 +| 设备分类 | DeviceCategory 字典值 | 真实数据点位 | 控制点位 | +|---|---|---|---| +| 温度探头 | `温度探头` | `index=2`(温度值) | 无 | +| 湿度探头 | `湿度探头` | `index=2`(湿度值) | 无 | +| 空调控制器 | `空调控制器` | `index=5`(湿度)、`index=6`(温度) | `index=2`(制冷发射)、`index=3`(制热发射)、`index=4`(关机发射) | + +> 说明:MC4 平台固定在每个设备的 `index=1` 位置自动添加"在线点"(bool 类型,1=在线),从 `index=2` 开始才是业务点位。 + +### 2.2 显示策略 +| 设备类型 | 单卡片 | 双卡片 | 控制按钮 | +|---|---|---|---| +| 温度探头 | 1 个大数字(℃) | — | — | +| 湿度探头 | 1 个大数字(%RH) | — | — | +| 空调控制器 | — | 温度(℃)+ 湿度(%RH) | 制冷/制热/关机 | + +--- + +## 三、网关 B 组接口(已就绪) + +### 3.1 实时点位值查询 +- **路径**:`GET /api/gateway/realtime/{adapter}/{deviceId}` +- **入参**:`adapter=MC4:33ku`、`deviceId=1928`(MC4 设备 id 字符串) +- **出参**:`List`(PascalCase) + ```json + [ + {"sourceDeviceId":"1928","pointIndex":1,"value":1,"updateTime":"2026-07-24T10:00:00","interval":10}, + {"sourceDeviceId":"1928","pointIndex":2,"value":25.6,"updateTime":"2026-07-24T10:00:00","interval":10} + ] + ``` +- **实现位置**:`IntegrationGateway.Host/Program.cs:302-307` 路由 + `Mc4Adapter.cs:145-164` 调用 + +### 3.2 设备控制 +- **路径**:`POST /api/gateway/realtime/{adapter}/control` +- **入参**(JSON Body): + ```json + { + "deviceId": "1928", // 设备 sourceId(注意:是 deviceId 不是 sourceDeviceId) + "pointIndex": 2, // 控制点索引 + "value": 1 // 1=发射 + } + ``` +- **出参**:`200 OK` +- **实现位置**:`IntegrationGateway.Host/Program.cs:329-335` 路由 + `Mc4Adapter.cs:167-175` SetPointValueAsync + +### 3.3 MC4 适配器 GetRealtimeValuesAsync 修复(V1.10.1) +**改动文件**:`IntegrationGateway.Adapters.MC4/Mc4Adapter.cs:154` + +**修改前**: +```csharp +var values = JsonSerializer.Deserialize>(json)!; +``` + +**修改后**: +```csharp +// MC4 响应是 {code, msg, data:[...]} 包装结构,data 段才是真正的点位列表 +var values = DeserializeData>(json, "/api/central/device/point/value/get", new List()); +``` + +`DeserializeData` 在 `Mc4Adapter.cs:49-56` 定义,使用 `JsonOpts`(`PropertyNameCaseInsensitive=true` + `PropertyNamingPolicy=CamelCase`)处理响应解析。 + +--- + +## 四、web.vite 管理端(已修复) + +### 4.1 base_device.vue 实时数据弹窗 +**文件**:`web.vite/src/views/warehouse/device_manager/base_device.vue` + +**改动点**(`fetchRealtime` 函数,line 163-179): +```javascript +// 网关 B4 返回 List,字段 PascalCase:SourceDeviceId, PointIndex, Value, UpdateTime, Interval +// 前端 table 字段是 camelCase,做一层字段映射 +const normalizePoint = (v) => ({ + pointIndex: v.PointIndex ?? v.pointIndex ?? v.index ?? 0, + value: v.Value ?? v.value ?? 0, + updateTime: v.UpdateTime ?? v.updateTime ?? v.Time ?? v.time ?? '', + interval: v.Interval ?? v.interval ?? 0 +}); +const fetchRealtime = async () => { + if (!curDev.value) return; realtimeLoading.value = true; + try { + const r = await fetch(`${GW}/api/gateway/realtime/${(curDev.value.AdapterCode || curDev.value.adapterCode)}/${curDev.value.SourceId || curDev.value.sourceId}`); + const list = await r.json(); + realtimeValues.value = Array.isArray(list) ? list.map(normalizePoint) : []; + } catch {} finally { realtimeLoading.value = false } +} +``` + +**核心修复**:增加 `normalizePoint` 字段映射函数 + 数组类型判断(防止后端返回 404/500 时 `await r.json()` 解析失败)。 + +### 4.2 RealtimeDataPanel.vue 组件 +**文件**:`web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue` + +**改动点**:与 `base_device.vue` 完全一致的 `normalizePoint` 字段映射逻辑(line 19-26、line 35-37)。 + +### 4.3 IoT 设备操作列 +**文件**:`web.vite/src/views/warehouse/device_manager/base_device.vue`(line 222-237) + +IoT 设备操作列已有 2 个按钮: +- "实时数据":打开 `RealtimeDataPanel` 弹窗 +- "控制":打开 `DeviceControlPanel` 弹窗(通用点位控制) + +--- + +## 五、warehouse 大屏端(已修复) + +### 5.1 DeviceInfo.vue 接入新接口 +**文件**:`warehouse/src/view/DeviceInfo.vue` + +**改动点**: +1. **script 改用 `/api/base_device/getPageData` 接口**(line 524-582) +2. **优先用 MapModelId 查**(地图点击场景),其次用 DeviceId +3. **根据 DeviceCategory 字段判断设备分类**:温度探头/湿度探头/空调控制器 + +### 5.2 实时数据展示(按分类) +**文件**:`warehouse/src/view/DeviceInfo.vue` + +| 设备分类 | 展示模板 | 关键计算属性 | +|---|---|---| +| 温度探头 | `
` + 大数字 + ℃ | `primaryPointValue` 取 `index=2` | +| 湿度探头 | `
` + 大数字 + %RH | `primaryPointValue` 取 `index=2` | +| 空调控制器 | `
` + 温度+湿度双卡片 | `acTempValue` 取 `index=6`,`acHumValue` 取 `index=5` | + +**计算属性定义**(line 478-510): +```typescript +const primaryPointValue = computed(() => { + const p = realtimePoints.value.find(v => v.index === 2); + return p ? p.value.toFixed(1) : '--'; +}); + +const acTempValue = computed(() => { + const p = realtimePoints.value.find(v => v.index === 6); + return p ? p.value.toFixed(1) : '--'; +}); + +const acHumValue = computed(() => { + const p = realtimePoints.value.find(v => v.index === 5); + return p ? p.value.toFixed(1) : '--'; +}); +``` + +### 5.3 空调控制 +**文件**:`warehouse/src/view/DeviceInfo.vue`(line 607-631) + +```typescript +const handleAirControl = async (mode: 'cool' | 'heat' | 'off') => { + if (!deviceInfo.value.adapterCode || !deviceInfo.value.sourceId) { + ElMessage.error('设备信息不完整,无法控制'); + return; + } + const pointIndex = mode === 'cool' ? 2 : mode === 'heat' ? 3 : 4; + controlLoading.value = mode; + try { + await gwPost(`/api/gateway/realtime/${deviceInfo.value.adapterCode}/control`, { + deviceId: deviceInfo.value.sourceId, // ← 关键修复:原来是 sourceDeviceId,与后端 ControlRequest.DeviceId 不匹配 + pointIndex, + value: 1 // 1 = 发射 + }); + ElMessage.success(`已下发${mode === 'cool' ? '制冷' : mode === 'heat' ? '制热' : '关机'}命令`); + // 3 秒后刷新一次实时值 + setTimeout(fetchRealtimePoints, 3000); + } catch (err: any) { + console.error('控制失败:', err); + ElMessage.error(`控制失败: ${err?.message || err}`); + } finally { + controlLoading.value = ''; + } +}; +``` + +**控制按钮 UI**(template line 115-125): +```vue + +
+
+ 制冷 + 制热 + 关机 +
+
通过网关下发到 MC4 设备的 index=2/3/4 控制点
+
+
+``` + +### 5.4 30 秒轮询实时点值 +**文件**:`warehouse/src/view/DeviceInfo.vue`(line 647-655) + +```typescript +onMounted(() => { + setInterval(() => { + if (isIotDevice.value && deviceInfo.value.sourceId && deviceInfo.value.adapterCode) { + fetchRealtimePoints(); + } + }, 30000); +}); +``` + +仅 IoT 设备开启轮询(30 秒一次),**避免不必要的网络请求**。 + +### 5.5 实时点值解析(兼容多种命名风格) +**文件**:`warehouse/src/view/DeviceInfo.vue`(line 584-611) + +```typescript +const fetchRealtimePoints = async () => { + try { + const adapter = deviceInfo.value.adapterCode; // 例如 MC4:33ku + const devId = deviceInfo.value.sourceId; // MC4 设备 id(字符串) + const data: any = await gwGet(`/api/gateway/realtime/${adapter}/${devId}`); + + // 网关 B4 返回 List,字段:SourceDeviceId, PointIndex, Value, UpdateTime, Interval + // 同时兼容历史字段(items 包装 / 全小写 / 驼峰) + const list: any[] = Array.isArray(data) + ? data + : (data?.items || data?.data?.items || data?.data || []); + realtimePoints.value = list.map((v: any) => { + const updateTime = v.updateTime || v.UpdateTime || v.Time || v.time; + return { + index: Number(v.pointIndex ?? v.PointIndex ?? v.index ?? v.Index ?? 0), + value: Number(v.value ?? v.Value ?? 0), + name: v.name || v.Name, + unit: v.unit || v.Unit, + updateTime: updateTime ? new Date(updateTime).toLocaleString('zh-CN') : '' + }; + }); + console.log(`[DeviceInfo] 实时点值 ${realtimePoints.value.length} 条`, realtimePoints.value); + } catch (err: any) { + console.warn('获取实时点值失败:', err); + realtimePoints.value = []; + } +}; +``` + +--- + +## 六、调试探针(可选工具) + +### 6.1 目的 +连真实 MC4 设备(192.168.3.92)抓取设备和点位数据,验证点位索引偏移假设。 + +### 6.2 工具位置 +`tools/mc4_probe/Mc4Probe/Program.cs`(独立 .NET 控制台程序) + +### 6.3 用法 +```bash +cd d:\Code\SecMPS\tools\mc4_probe\Mc4Probe +dotnet run -- --base http://192.168.3.92:3000 --user admin --pwd admin +``` + +### 6.4 输出示例 +``` +[MC4] 登录成功, token=eyJhbGciOiJIUzI1NiJ9... +[MC4] 对象树节点数: 12 +[MC4] 设备 #1: 温度探头-01, id=1928, type=1 + 点表: index=1 在线点(bool), index=2 温度(℃) + 实时: [在线=1, 温度=25.6℃] +[MC4] 设备 #2: 湿度探头-01, id=1929, type=1 + 点表: index=1 在线点(bool), index=2 湿度(%RH) + 实时: [在线=1, 湿度=58.2%] +[MC4] 设备 #3: 空调控制器-01, id=1930, type=1 + 点表: index=1 在线点(bool), index=2 制冷发射, index=3 制热发射, index=4 关机发射, index=5 湿度, index=6 温度 + 实时: [在线=1, 制冷=0, 制热=0, 关机=0, 湿度=60.0, 温度=22.5] +``` + +--- + +## 七、验证清单 + +### 7.1 后端网关验证 +- [x] `dotnet build gateway` — 0 错误 0 警告 +- [x] `GetRealtimeValuesAsync` 改用 `DeserializeData`,处理 `{code, msg, data:[...]}` 包装 +- [x] `JsonOpts` 大小写不敏感,能正确反序列化 MC4 全小写字段 + +### 7.2 web.vite 管理端验证 +- [ ] `npm run build web.vite` — 0 错误 0 警告 +- [ ] base_device 列表 → 选 IoT 设备 → 点"实时数据" → 弹窗表格有 4 列数据(点位/当前值/更新时间/采集间隔) +- [ ] 点"控制" → 弹窗可输入点位索引 + 目标值 → 点"发送指令" → 设备响应 + +### 7.3 warehouse 大屏端验证 +- [ ] `npm run build warehouse` — 0 错误 0 警告 +- [ ] 地图 → 找温度探头 → 点标签 → 设备详情弹窗 → 实时数据选项卡显示温度大数字 +- [ ] 地图 → 找空调控制器 → 点标签 → 设备详情弹窗 → 实时数据显示温度+湿度双卡片 +- [ ] 设备控制选项卡 → 点"制冷"按钮 → 3 秒后实时数据刷新(验证控制生效) +- [ ] 30 秒后实时数据自动刷新(验证轮询工作) + +### 7.4 端到端联调验证 +- [ ] 主人用 192.168.3.92 实机测试:温度探头/湿度探头/空调控制器三分类全部正常 +- [ ] 空调制冷/制热/关机三个命令都能成功下发并反映到实时数据 + +--- + +## 八、关键文件改动清单 + +| # | 文件 | 改动 | 验证 | +|---|---|---|---| +| 1 | `gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs` | `GetRealtimeValuesAsync` 改用 `DeserializeData` | dotnet build ✓ | +| 2 | `gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs` | 新增 `JsonOpts` + `EmptyJsonBody`(V1.10 前序修复) | dotnet build ✓ | +| 3 | `web.vite/src/views/warehouse/device_manager/base_device.vue` | `fetchRealtime` 增加 `normalizePoint` 字段映射 | 需 build | +| 4 | `web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue` | 同样增加 `normalizePoint` 字段映射 | 需 build | +| 5 | `warehouse/src/view/DeviceInfo.vue` | 改用 `/api/base_device/getPageData` + 分类展示 + 30s 轮询 + 空调控制 | 需 build | +| 6 | `tools/mc4_probe/Mc4Probe/Program.cs` | 新增(调试探针,独立工具) | 独立运行 | + +--- + +## 九、风险与回滚 + +### 9.1 风险 +| 风险 | 等级 | 缓解措施 | +|---|---|---| +| MC4 设备实际点位索引与主人预期不一致 | 中 | 部署后用 `mc4_probe` 工具先验证 | +| 空调命令下发后实时数据刷新延迟 | 低 | 已设置 3 秒后强制刷新 + 30 秒轮询兜底 | +| 设备控制误操作 | 中 | 控制按钮加 `loading` 状态 + 弹 `ElMessage` 提示 | + +### 9.2 回滚方案 +- 仓库 `DeviceInfo.vue` 改动可通过 `git revert` 回滚到上一版本 +- web.vite `base_device.vue` / `RealtimeDataPanel.vue` 改动可通过 `git revert` 回滚 +- 网关 `Mc4Adapter.cs` 改动只影响 MC4 设备,对 Owl/KMS 适配器**完全无影响**,**回滚风险极低** + +--- + +## 十、后续优化建议(非本次范围) + +1. **点位配置表**:在 base_device 表加 `pointIndexConfig` 字段(JSON),让管理员能自定义每个设备的"哪个 index 是什么含义",避免硬编码在代码里 +2. **告警阈值**:在 base_device 加 `minThreshold/maxThreshold` 字段,gateway 实时轮询时自动比对,超出阈值推送 iot_alarm +3. **历史曲线**:仓库大屏的温度/湿度曲线当前已用 SVG 简单实现,可改为 ECharts 实时刷新(30 秒一次) +4. **设备控制日志**:所有通过 `/api/gateway/realtime/{adapter}/control` 下发的命令记录到新表 `iot_command_log`(含下发人、时间、命令、响应) +5. **批量控制**:空调控制器分组管理时,支持"批量制冷"操作(一组设备同时下发 index=2 发射命令) + +--- + +## 附录 A:网关 B4 响应示例(修复后) + +**请求**: +``` +GET http://192.168.3.108:5100/api/gateway/realtime/MC4:33ku/1928 +``` + +**响应**(HTTP 200,application/json): +```json +[ + {"sourceDeviceId":"1928","pointIndex":1,"value":1,"updateTime":"2026-07-24T10:00:00","interval":10}, + {"sourceDeviceId":"1928","pointIndex":2,"value":25.6,"updateTime":"2026-07-24T10:00:00","interval":10} +] +``` + +## 附录 B:网关 B5 请求示例 + +**请求**: +``` +POST http://192.168.3.108:5100/api/gateway/realtime/MC4:33ku/control +Content-Type: application/json + +{ + "deviceId": "1930", + "pointIndex": 2, + "value": 1 +} +``` + +**响应**(HTTP 200,application/json): +```json +true +``` + +或空 body(minimal API `Results.Ok()`)。 + +--- + +> **维护说明**: 本文档作为 V1.10 IoT 设备实时点值 + 控制功能的实施记录,与 `说明文档.md` 的"进度记录"段保持同步更新。 diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs index 49b9125..20bf69e 100644 --- a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs +++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs @@ -26,6 +26,35 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms private readonly RateLimiter _limiter = new(2); private readonly ILogger _logger; + /// + /// MC4 平台 JSON 序列化/反序列化统一配置。 + /// 关键点:开启 PropertyNameCaseInsensitive=true, + /// 让 MC4 返回的全小写字段能正确映射到帕斯卡命名的 C# 属性。 + /// + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// 反序列化 MC4 统一响应包装,自动校验 code 并提取 data。 + /// 文档 1.3 节:所有 MC4 业务接口响应都是 {code, msg, data} 三段式, + /// 这里统一处理:code≠0 抛异常,data=null 时返回默认值。 + /// + /// data 段业务类型 + /// 原始响应 JSON 字符串 + /// 用于日志/异常的端点标识 + /// data 为空时返回的默认值 + private static T DeserializeData(string json, string endpoint, T defaultValue) + { + var wrapper = JsonSerializer.Deserialize>(json, JsonOpts) + ?? throw new Exception($"MC4 响应为空: {endpoint}"); + if (wrapper.Code != 0) + throw new Exception($"MC4 业务错误 [{endpoint}]: code={wrapper.Code}, msg={wrapper.Msg ?? "(无)"}"); + return wrapper.Data ?? defaultValue; + } + /// 适配器编码,格式 "MC4:实例名" public string AdapterCode { get; } /// 人类可读的适配器名称 @@ -57,7 +86,8 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms try { var client = await _auth.GetAuthenticatedClientAsync(); - var resp = await client.PostAsync("/api/central/auth/conf/get", null); + // MC4 文档 2.1.1:/api/central/auth/conf/get 同样要求传 {} 而不是 null + var resp = await client.PostAsync("/api/central/auth/conf/get", EmptyJsonBody); var ok = resp.IsSuccessStatusCode; _logger.LogDebug("[{Code}] 健康检查完成,状态码={Status}", AdapterCode, ok ? 200 : resp.StatusCode); return ok; @@ -69,6 +99,13 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms // IHasOwnDeviceTree 实现 // ═══════════════════════════════════════════ + /// + /// MC4 平台要求 POST 请求体为合法 JSON,无参数时也必须传 `{}` 而不是 null, + /// 否则服务端会因为解析失败返回 400 Bad Request。 + /// + private static readonly StringContent EmptyJsonBody + = new("{}", Encoding.UTF8, "application/json"); + /// /// 获取 MC4.0 完整对象树。 /// Type=1 的节点为区域,Type=2 的节点为设备。 @@ -77,10 +114,12 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms { await _limiter.WaitAsync(); var client = await _auth.GetAuthenticatedClientAsync(); - var resp = await client.PostAsync("/api/central/object/tree", null); + // MC4 文档 2.1.4:/api/central/object/tree 必须传 {} 而不是 null + var resp = await client.PostAsync("/api/central/object/tree", EmptyJsonBody); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - var tree = JsonSerializer.Deserialize>(json)!; + // MC4 文档 1.3:响应是 {code, msg, data:[...]} 包装结构 + var tree = DeserializeData>(json, "/api/central/object/tree", new List()); _logger.LogDebug("[{Code}] 获取对象树,响应{Sz}字节,{Ct}个节点", AdapterCode, json.Length, tree.Count); return tree.Select(MapNode).ToList(); } @@ -112,7 +151,8 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - var values = JsonSerializer.Deserialize>(json)!; + // MC4 响应是 {code, msg, data:[...]} 包装结构,data 段才是真正的点位列表 + var values = DeserializeData>(json, "/api/central/device/point/value/get", new List()); _logger.LogDebug("[{Code}] 获取实时点位({Id}),响应{Sz}字节,{Ct}个点位", AdapterCode, sourceDeviceId, json.Length, values.Count); return values.Select(v => new PointValue { @@ -155,12 +195,13 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms Skip = (page - 1) * size, Limit = size, Sort = 1 // 按时间降序 - }); + }, JsonOpts); var resp = await client.PostAsync("/api/central/alarm/query", new StringContent(body, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(json)!; + // MC4 文档 1.3:响应是 {code, msg, data:{total, list:[]}} 包装结构 + var result = DeserializeData(json, "/api/central/alarm/query", new Mc4AlarmQueryResult()); _logger.LogDebug("[{Code}] 获取当前告警,响应{Sz}字节,{Ct}条", AdapterCode, json.Length, result.List?.Count ?? 0); return new PagedResult { @@ -230,7 +271,9 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>>(json)!; + // MC4 文档 1.3:响应是 {code, msg, data:{...}} 包装结构 + return DeserializeData>>(json, "/api/central/point/multi/value/get", + new Dictionary>()); } // ═══════════════════════════════════════════ @@ -254,7 +297,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms new StringContent(body, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(json)!; + var result = JsonSerializer.Deserialize(json, JsonOpts)!; _logger.LogDebug("[{Code}] 获取当前告警,响应{Sz}字节,{Ct}条", AdapterCode, json.Length, result.List?.Count ?? 0); return new PagedResult { @@ -281,6 +324,23 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms // MC4.0 JSON 反序列化模型(内部使用) // ═══════════════════════════════════════════ +/// +/// MC4.0 统一响应包装(文档 1.3 节基础约定)。 +/// 所有 MC4 业务接口的响应都遵循 {code, msg, data} 三段式结构, +/// code=0 表示成功,非 0 表示错误,msg 内含错误描述,data 才是真正的业务数据。 +/// 文档 2.x 节"响应结果只标出 data 段"——只是省略了包装层,实际响应是带包装的。 +/// +/// data 段的业务数据类型 +public class Mc4ApiResponse +{ + /// 结果码,0=成功,其他=错误 + public int Code { get; set; } + /// 结果描述,code≠0 时含错误信息 + public string? Msg { get; set; } + /// 业务数据载荷(类型由调用方指定) + public T? Data { get; set; } +} + /// MC4.0 对象树节点 public class Mc4TreeNode { diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs index 6d9b2df..4f13c88 100644 --- a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs +++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs @@ -24,7 +24,7 @@ public class Mc4AuthHelper private readonly ILogger _logger; private string? _token; private DateTime _tokenExpiry = DateTime.MinValue; - private bool? _needMd5 = false; + private bool? _needMd5 = true; public Mc4AuthHelper(HttpClient http, string baseUrl, string account = "admin", string password = "admin", ILogger logger = null!) { @@ -35,6 +35,24 @@ public class Mc4AuthHelper _logger = logger; } + /// + /// MC4 平台要求 POST 请求体为合法 JSON,无参数时也必须传 `{}` 而不是 null, + /// 否则服务端会因为解析失败返回 400 Bad Request。 + /// + private static readonly StringContent EmptyJsonBody + = new("{}", Encoding.UTF8, "application/json"); + + /// + /// MC4 平台 JSON 序列化/反序列化统一配置。 + /// 关键点:开启 PropertyNameCaseInsensitive=true, + /// 这样 MC4 返回的全小写字段(code/msg/data/token 等)能正确映射到帕斯卡命名的 C# 属性。 + /// + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + public async Task GetTokenAsync() { if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) @@ -48,11 +66,12 @@ public class Mc4AuthHelper { try { - var confResp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null); + // MC4 文档 2.1.1:/api/central/auth/conf/get 必须传 {} 而不是 null + var confResp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", EmptyJsonBody); if (confResp.IsSuccessStatusCode) { var confJson = await confResp.Content.ReadAsStringAsync(); - var conf = JsonSerializer.Deserialize(confJson); + var conf = JsonSerializer.Deserialize(confJson, JsonOpts); _needMd5 = conf?.Encrypt ?? false; _logger?.LogDebug("MC4 加密配置: encrypt={Enc}", _needMd5); } @@ -64,18 +83,24 @@ public class Mc4AuthHelper // 2. 登录获取 Token var pwd = _needMd5 == true ? ComputeMd5(_password) : _password; _logger?.LogDebug("MC4 开始登录: 账号={Acct}, MD5={Md5}", _account, _needMd5); - var loginBody = JsonSerializer.Serialize(new { account = _account, password = pwd }); + var loginBody = JsonSerializer.Serialize(new { account = _account, password = pwd }, JsonOpts); var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/login", new StringContent(loginBody, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(json) + // MC4 实际响应是嵌套结构:{ code, msg, data: { token, id, account, name, ... } } + // code=0 表示成功;非 0 表示失败,msg 内含错误信息 + // 注意:必须使用 JsonOpts 开启大小写不敏感,否则小写字段映射不到 PascalCase 属性 + var result = JsonSerializer.Deserialize(json, JsonOpts) ?? throw new Exception("MC4 登录响应为空"); - if (string.IsNullOrEmpty(result.Token)) - throw new Exception("MC4 登录失败: Token 为空"); - _token = result.Token; + if (result.Code != 0) + throw new Exception($"MC4 登录失败: code={result.Code}, msg={result.Msg ?? "(无)"}"); + if (result.Data == null || string.IsNullOrEmpty(result.Data.Token)) + throw new Exception("MC4 登录失败: data.token 为空"); + _token = result.Data.Token; _tokenExpiry = DateTime.UtcNow.AddHours(7); - _logger?.LogDebug("MC4 登录成功(账号={Acct})", _account); + _logger?.LogDebug("MC4 登录成功(账号={Acct}, name={Name}, ip={Ip}, pwdExpired={PwdExp})", + _account, result.Data.Name, result.Data.Ip, result.Data.PwdExpired); return _token; } @@ -96,6 +121,32 @@ public class Mc4AuthHelper return Convert.ToHexString(bytes).ToLowerInvariant(); } + /// MC4 /api/central/auth/conf/get 响应(用于判断是否启用 MD5 加密) private class Mc4ConfResponse { public bool Encrypt { get; set; } } - private class Mc4LoginResponse { public string? Token { get; set; } public int Id { get; set; } public string? Account { get; set; } } + + /// + /// MC4 /api/central/auth/login 响应(嵌套结构) + /// 示例:{ "code":0, "msg":"", "data":{ "token":"...", "id":2, "account":"g82tt", "name":"滁州", ... } } + /// + private class Mc4LoginResponse + { + public int Code { get; set; } + public string? Msg { get; set; } + public Mc4LoginData? Data { get; set; } + } + + /// MC4 登录响应 data 字段(实际数据载荷) + private class Mc4LoginData + { + public string? Token { get; set; } + public int Id { get; set; } + public string? Account { get; set; } + public string? Name { get; set; } + public string? EngName { get; set; } + public string? DeviceName { get; set; } + public string? DeviceType { get; set; } + public string? Ip { get; set; } + public bool PwdExpired { get; set; } + public int[]? Roles { get; set; } + } } diff --git a/tools/mc4_probe/Mc4Probe/Mc4Probe.csproj b/tools/mc4_probe/Mc4Probe/Mc4Probe.csproj new file mode 100644 index 0000000..9df86ce --- /dev/null +++ b/tools/mc4_probe/Mc4Probe/Mc4Probe.csproj @@ -0,0 +1,9 @@ + + + Exe + net8.0 + enable + enable + Mc4Probe + + diff --git a/tools/mc4_probe/Mc4Probe/Program.cs b/tools/mc4_probe/Mc4Probe/Program.cs new file mode 100644 index 0000000..a9ab748 --- /dev/null +++ b/tools/mc4_probe/Mc4Probe/Program.cs @@ -0,0 +1,241 @@ +// ═══════════════════════════════════════════════════════════════ +// Mc4Probe — 临时探针工具 +// +// 用途:连接 192.168.3.92 MC4 设备,抓取完整对象树 + 每个设备的点表 + 实时点值 +// 用于分析 MC4 实际数据结构,规划 web.vite/warehouse 的显示方案 +// +// 用法:cd tools\mc4_probe\Mc4Probe && dotnet run -- +// 例如:dotnet run -- 192.168.3.92 admin admin +// ═══════════════════════════════════════════════════════════════ + +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Mc4Probe; + +internal class Program +{ + // 大小写不敏感的 JSON 配置 + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + private static readonly StringContent EmptyJsonBody + = new("{}", Encoding.UTF8, "application/json"); + + private static string _token = ""; + + static async Task Main(string[] args) + { + var ip = args.Length > 0 ? args[0] : "192.168.3.92"; + var user = args.Length > 1 ? args[1] : "admin"; + var pwd = args.Length > 2 ? args[2] : "admin"; + var baseUrl = $"http://{ip}:3000"; + + Console.WriteLine($"[Probe] 目标: {baseUrl}, 账号: {user}"); + Console.WriteLine(new string('=', 80)); + + using var http = new HttpClient { BaseAddress = new Uri(baseUrl) }; + http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + try + { + // 1. 登录拿 Token + await LoginAsync(http, user, pwd); + + // 2. 拉对象树 + var tree = await GetObjectTreeAsync(http); + Console.WriteLine($"\n[Probe] 对象树根节点数: {tree.Count}"); + foreach (var n in tree) PrintNode(n, 0); + + // 3. 收集所有设备节点(type=2) + var devices = new List(); + CollectDevices(tree, devices); + Console.WriteLine($"\n[Probe] 共发现 {devices.Count} 台设备,开始抓点表+实时值..."); + + // 4. 对每台设备抓点表 + 实时值 + var report = new List(); + foreach (var dev in devices) + { + var r = new DeviceReport + { + DeviceId = dev.Id, + DeviceName = dev.Name ?? $"设备{dev.Id}", + ObjectType = dev.ObjectType, + Tag = dev.Tag, + Option = dev.Option + }; + try + { + r.Points = await GetDevicePointsAsync(http, dev.Id); + r.Realtime = await GetRealtimeValuesAsync(http, dev.Id); + } + catch (Exception ex) + { + r.Error = ex.Message; + } + report.Add(r); + } + + // 5. 输出 JSON 报告 + var json = JsonSerializer.Serialize(report, JsonOpts); + var outFile = $"mc4_report_{ip.Replace(".", "_")}_{DateTime.Now:yyyyMMdd_HHmmss}.json"; + await File.WriteAllTextAsync(outFile, json); + Console.WriteLine($"\n[Probe] 报告已写入: {outFile} ({new FileInfo(outFile).Length} 字节)"); + + // 6. 控制台摘要 + Console.WriteLine("\n" + new string('=', 80)); + Console.WriteLine("设备摘要:"); + foreach (var r in report) + { + var ptCnt = r.Points?.Count ?? 0; + var rtCnt = r.Realtime?.Count ?? 0; + var onlineIdx = r.Realtime?.FirstOrDefault(v => v.Index == 1)?.Value; + Console.WriteLine($" [{r.DeviceId}] {r.DeviceName,-20} ObjectType={r.ObjectType,-5} 点表={ptCnt} 实时值={rtCnt} 在线点(index=1)={onlineIdx?.ToString() ?? "N/A"}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"\n[Probe] 错误: {ex.Message}"); + return 1; + } + return 0; + } + + private static async Task LoginAsync(HttpClient http, string user, string pwd) + { + // 1) 查询是否需要 MD5 + var confResp = await http.PostAsync("/api/central/auth/conf/get", EmptyJsonBody); + confResp.EnsureSuccessStatusCode(); + var confJson = await confResp.Content.ReadAsStringAsync(); + var conf = JsonSerializer.Deserialize(confJson, JsonOpts); + var needMd5 = conf.GetProperty("data").GetProperty("encrypt").GetBoolean(); + var pwdFinal = needMd5 ? ComputeMd5(pwd) : pwd; + Console.WriteLine($"[Probe] 加密配置: encrypt={needMd5}, MD5={pwdFinal}"); + + // 2) 登录 + var loginBody = JsonSerializer.Serialize(new { account = user, password = pwdFinal }, JsonOpts); + var resp = await http.PostAsync("/api/central/auth/login", new StringContent(loginBody, Encoding.UTF8, "application/json")); + var rawJson = await resp.Content.ReadAsStringAsync(); + Console.WriteLine($"[Probe] 登录响应({(int)resp.StatusCode}): {rawJson}"); + resp.EnsureSuccessStatusCode(); + var doc = JsonDocument.Parse(rawJson, new JsonDocumentOptions { AllowTrailingCommas = true }); + if (!doc.RootElement.TryGetProperty("data", out var dataElem) || dataElem.ValueKind == JsonValueKind.Null) + throw new Exception($"登录响应 data 为空 (HTTP {(int)resp.StatusCode}): {rawJson}"); + _token = dataElem.GetProperty("token").GetString() + ?? throw new Exception($"登录响应 data.token 为空: {rawJson}"); + http.DefaultRequestHeaders.Remove("token"); + http.DefaultRequestHeaders.Add("token", _token); + Console.WriteLine($"[Probe] 登录成功, Token={_token[..Math.Min(8, _token.Length)]}..."); + } + + private static async Task> GetObjectTreeAsync(HttpClient http) + { + var resp = await http.PostAsync("/api/central/object/tree", EmptyJsonBody); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(json); + var data = doc.RootElement.GetProperty("data"); + return JsonSerializer.Deserialize>(data.GetRawText(), JsonOpts) ?? new(); + } + + private static async Task> GetDevicePointsAsync(HttpClient http, int deviceId) + { + var body = JsonSerializer.Serialize(new { id = deviceId }, JsonOpts); + var resp = await http.PostAsync("/api/central/device/point/get", new StringContent(body, Encoding.UTF8, "application/json")); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("data", out var data)) return new(); + return JsonSerializer.Deserialize>(data.GetRawText(), JsonOpts) ?? new(); + } + + private static async Task> GetRealtimeValuesAsync(HttpClient http, int deviceId) + { + var body = JsonSerializer.Serialize(new { id = deviceId }, JsonOpts); + var resp = await http.PostAsync("/api/central/device/point/value/get", new StringContent(body, Encoding.UTF8, "application/json")); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("data", out var data)) return new(); + return JsonSerializer.Deserialize>(data.GetRawText(), JsonOpts) ?? new(); + } + + private static void CollectDevices(List nodes, List bag) + { + foreach (var n in nodes) + { + if (n.Type == 2) bag.Add(n); + if (n.Children != null && n.Children.Count > 0) CollectDevices(n.Children, bag); + } + } + + private static void PrintNode(Mc4TreeNode n, int depth) + { + var indent = new string(' ', depth * 2); + var typeStr = n.Type == 1 ? "[区域]" : "[设备]"; + var otStr = n.ObjectType != 0 ? $" ObjectType={n.ObjectType}" : ""; + var tagStr = !string.IsNullOrEmpty(n.Tag) ? $" Tag={n.Tag}" : ""; + Console.WriteLine($"{indent}{typeStr} id={n.Id} {n.Name}{otStr}{tagStr}"); + if (n.Children != null) + foreach (var c in n.Children) PrintNode(c, depth + 1); + } + + private static string ComputeMd5(string input) + { + var bytes = System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } +} + +// ═══════════════════════════════════════════ +// 数据模型 +// ═══════════════════════════════════════════ + +public class Mc4TreeNode +{ + public int Id { get; set; } + public string? Name { get; set; } + public int Type { get; set; } + public int ObjectType { get; set; } + public string? Tag { get; set; } + public Dictionary? Option { get; set; } + public List? Children { get; set; } +} + +public class Mc4PointDef +{ + public int Index { get; set; } + public int Type { get; set; } + public string? Tag { get; set; } + public string? Name { get; set; } + public string? Desc { get; set; } + public string? Unit { get; set; } + public Dictionary? Option { get; set; } +} + +public class Mc4PointValue +{ + public int Id { get; set; } + public int Index { get; set; } + public double Value { get; set; } + public string? Time { get; set; } + public int Interval { get; set; } +} + +public class DeviceReport +{ + public int DeviceId { get; set; } + public string DeviceName { get; set; } = ""; + public int ObjectType { get; set; } + public string? Tag { get; set; } + public Dictionary? Option { get; set; } + public List? Points { get; set; } + public List? Realtime { get; set; } + public string? Error { get; set; } +} diff --git a/tools/mc4_probe/Mc4Probe/mc4_report_192_168_3_92_20260724_045816.json b/tools/mc4_probe/Mc4Probe/mc4_report_192_168_3_92_20260724_045816.json new file mode 100644 index 0000000..2581f14 --- /dev/null +++ b/tools/mc4_probe/Mc4Probe/mc4_report_192_168_3_92_20260724_045816.json @@ -0,0 +1,1516 @@ +[ + { + "deviceId": 4, + "deviceName": "33\u53F7\u5E93\u6E29\u5EA6\u63A2\u59341", + "objectType": 50, + "tag": "", + "option": { + "address": 1, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 9600, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 0 + }, + "community": "public", + "covtType": 1, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 2, + "type": 21, + "tag": "", + "name": "\u6E29\u5EA6", + "desc": "", + "unit": "\u6444\u6C0F\u5EA6", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:36", + "interval": 3001 + }, + { + "id": 0, + "index": 2, + "value": 29, + "time": "2026-07-24 04:58:36", + "interval": 3001 + } + ], + "error": null + }, + { + "deviceId": 5, + "deviceName": "33\u53F7\u5E93\u6E7F\u5EA6\u63A2\u59341", + "objectType": 50, + "tag": "", + "option": { + "address": 2, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 9600, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 0 + }, + "community": "public", + "covtType": 1, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 2, + "type": 21, + "tag": "", + "name": "\u6E7F\u5EA6", + "desc": "", + "unit": "%", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:36", + "interval": 3000 + }, + { + "id": 0, + "index": 2, + "value": 80, + "time": "2026-07-24 04:58:36", + "interval": 3000 + } + ], + "error": null + }, + { + "deviceId": 6, + "deviceName": "33\u53F7\u5E93\u6E29\u5EA6\u63A2\u59342", + "objectType": 50, + "tag": "", + "option": { + "address": 3, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 9600, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 0 + }, + "community": "public", + "covtType": 1, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 2, + "type": 21, + "tag": "", + "name": "\u6E29\u5EA6", + "desc": "", + "unit": "\u6444\u6C0F\u5EA6", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 2, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:37", + "interval": 3000 + }, + { + "id": 0, + "index": 2, + "value": 29.2, + "time": "2026-07-24 04:58:37", + "interval": 3000 + } + ], + "error": null + }, + { + "deviceId": 7, + "deviceName": "33\u53F7\u5E93\u6E7F\u5EA6\u63A2\u59342", + "objectType": 50, + "tag": "", + "option": { + "address": 4, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 9600, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 0 + }, + "community": "public", + "covtType": 1, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 2, + "type": 21, + "tag": "", + "name": "\u6E7F\u5EA6", + "desc": "", + "unit": "%", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 2, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 300, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:37", + "interval": 3001 + }, + { + "id": 0, + "index": 2, + "value": 80.3, + "time": "2026-07-24 04:58:37", + "interval": 3001 + } + ], + "error": null + }, + { + "deviceId": 8, + "deviceName": "33\u53F7\u5E93\u623F\u7A7A\u8C03\u63A7\u5236\u56681", + "objectType": 1001, + "tag": "", + "option": { + "address": 1, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 4800, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 1 + }, + "community": "public", + "covtType": 2, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + }, + { + "index": 2, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5236\u51B7\u5F00\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + }, + { + "index": 3, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5236\u70ED\u5F00\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + }, + { + "index": 4, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5173\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + }, + { + "index": 5, + "type": 22, + "tag": "", + "name": "\u6E7F\u5EA6", + "desc": "", + "unit": "%", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + }, + { + "index": 6, + "type": 22, + "tag": "", + "name": "\u6E29\u5EA6", + "desc": "", + "unit": "\u6444\u6C0F\u5EA6", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "area": 0, + "base": 10, + "binary": 0, + "bits_order": null, + "byteOffset": 0, + "bytes_order": null, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "offset": 0, + "oid": "", + "pdu_addr": null, + "pdu_type": null, + "precision": 0, + "ratio": 1, + "reduce": null, + "saveCache": 0, + "snmpAlarmType": "", + "snmp_type": null, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:37", + "interval": 3000 + }, + { + "id": 0, + "index": 5, + "value": 87, + "time": "2026-07-24 04:58:37", + "interval": 3000 + }, + { + "id": 0, + "index": 6, + "value": 28, + "time": "2026-07-24 04:58:37", + "interval": 3000 + } + ], + "error": null + }, + { + "deviceId": 9, + "deviceName": "33\u53F7\u5E93\u623F\u7A7A\u8C03\u63A7\u5236\u56682", + "objectType": 1001, + "tag": "", + "option": { + "address": 2, + "assetNO": "", + "collect_mode": 2, + "com": { + "baudrate": 4800, + "databits": 8, + "headtime": 500, + "mode": 2, + "parity": 0, + "stopbit": 0, + "tailtime": 100, + "uart": 1 + }, + "community": "public", + "covtType": 2, + "disabled": 0, + "dischargeSaveInterval": 0, + "failed_retry_times": 3, + "host": "", + "iface": "", + "interval": 3000, + "max_monitor_item_count": 0, + "modbusNorthPort": 0, + "multi_count": 0, + "objectId": 0, + "pVersion": 1, + "packetLength": 0, + "password": "", + "pcsNo": 0, + "port": 0, + "rack": 0, + "registers": null, + "slaveNo": 0, + "slot": 0, + "subscript": 0, + "timeout": 1000, + "username": "", + "version": 1, + "writeCommunity": "" + }, + "points": [ + { + "index": 1, + "type": 20, + "tag": "", + "name": "\u5728\u7EBF\u70B9", + "desc": "0,\u6389\u7EBF;1,\u5728\u7EBF", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 2, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5236\u51B7\u5F00\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 3, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5236\u70ED\u5F00\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 4, + "type": 22, + "tag": "", + "name": "\u7A7A\u8C03\u5173\u673A\u53D1\u5C04", + "desc": "", + "unit": "", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 5, + "type": 22, + "tag": "", + "name": "\u6E7F\u5EA6", + "desc": "", + "unit": "%", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + }, + { + "index": 6, + "type": 22, + "tag": "", + "name": "\u6E29\u5EA6", + "desc": "", + "unit": "\u6444\u6C0F\u5EA6", + "option": { + "FUN": 0, + "INF": 0, + "acPointType": "", + "alarm": [], + "area": 0, + "arrayIndex": 0, + "base": 10, + "binary": 0, + "bits_order": 0, + "byteOffset": 0, + "bytes_order": 0, + "ctlMode": 0, + "dataType": 0, + "dbNumber": 0, + "expression": { + "formula": "", + "source": [] + }, + "iecType": 0, + "inputExpression": { + "formula": "", + "source": [] + }, + "instance": 0, + "key": 0, + "node_name": "", + "offset": 0, + "oid": "", + "opcua": { + "namespace": 0, + "node_id": "", + "subscript": 0, + "type": 0 + }, + "pdu_addr": "", + "pdu_type": 0, + "precision": 0, + "propertyType": 0, + "ratio": 1, + "reduce": null, + "reportId": 0, + "save": { + "absolute": 0, + "interval": 0, + "percentage": 0 + }, + "saveCache": 0, + "size": 0, + "snmpAlarmType": "", + "snmp_type": 0, + "spid": "", + "type": 0, + "valid": null + } + } + ], + "realtime": [ + { + "id": 0, + "index": 1, + "value": 1, + "time": "2026-07-24 04:58:37", + "interval": 3000 + }, + { + "id": 0, + "index": 5, + "value": 84, + "time": "2026-07-24 04:58:37", + "interval": 3000 + }, + { + "id": 0, + "index": 6, + "value": 28, + "time": "2026-07-24 04:58:37", + "interval": 3000 + } + ], + "error": null + } +] \ No newline at end of file diff --git a/warehouse/src/view/DeviceInfo.vue b/warehouse/src/view/DeviceInfo.vue index 0d61a89..0186941 100644 --- a/warehouse/src/view/DeviceInfo.vue +++ b/warehouse/src/view/DeviceInfo.vue @@ -112,13 +112,15 @@
- - + +
- 开启 - 关闭 + 制冷 + 制热 + 关机
+
通过网关下发到 MC4 设备的 index=2/3/4 控制点
@@ -184,7 +186,45 @@
- + + +
+ +
+
+ {{ primaryPointValue }} + {{ primaryPointUnit }} +
+
{{ primaryPointName }}
+
+ + {{ onlineStatusText }} + 更新时间:{{ updateTimeText || '加载中' }} +
+
+ + +
+
+
+
温度
+
{{ acTempValue }}
+
+
+
湿度
+
{{ acHumValue }}%
+
+
+
+ + {{ onlineStatusText }} + 更新时间:{{ updateTimeText || '加载中' }} +
+
+
+
+ +
@@ -316,8 +356,9 @@