feat: 添加回路名称字段并实现元数据缓存查询
在 RCU 事件处理中新增回路名称(loop_name)字段,用于标识具体设备回路。 - 在 rcu_action_events 表中添加 loop_name 字段 - 新增项目元数据缓存模块,每日从 temporary_project 表刷新房间与回路信息 - 处理消息时,根据 device_id、dev_addr 等字段查询缓存获取回路名称 - 若缓存未命中,则根据设备类型规则生成兜底名称 - 更新环境变量、文档及测试用例以适配新功能
This commit is contained in:
@@ -32,3 +32,5 @@ REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=15
|
||||
REDIS_CONNECT_TIMEOUT_MS=5000
|
||||
|
||||
ACTION_TYPE_DEV_TYPE_RULES='[{"dev_type":0,"name":"无效设备(也可以被认为是场景)","action_type":"无效"},{"dev_type":1,"name":"强电继电器(输出状态)","action_type":"设备回路状态"},{"dev_type":2,"name":"弱电输入(输入状态)","action_type":"用户操作"},{"dev_type":3,"name":"弱电输出(输出状态)","action_type":"设备回路状态"},{"dev_type":4,"name":"服务信息","action_type":"设备回路状态"},{"dev_type":5,"name":"干节点窗帘","action_type":"设备回路状态"},{"dev_type":6,"name":"开关","action_type":"用户操作"},{"dev_type":7,"name":"空调","action_type":"用户操作"},{"dev_type":8,"name":"红外感应","action_type":"用户操作"},{"dev_type":9,"name":"空气质量检测设备","action_type":"设备回路状态"},{"dev_type":10,"name":"插卡取电","action_type":"用户操作"},{"dev_type":11,"name":"地暖","action_type":"用户操作"},{"dev_type":12,"name":"RCU 设备网络 - 没使用","action_type":""},{"dev_type":13,"name":"窗帘","action_type":"设备回路状态"},{"dev_type":14,"name":"继电器","action_type":"设备回路状态"},{"dev_type":15,"name":"红外发送","action_type":"设备回路状态"},{"dev_type":16,"name":"调光驱动","action_type":"设备回路状态"},{"dev_type":17,"name":"可控硅调光(可控硅状态)","action_type":"设备回路状态"},{"dev_type":18,"name":"灯带(灯带状态) --2025-11-24 取消","action_type":"无效"},{"dev_type":19,"name":"中控","action_type":"无效"},{"dev_type":20,"name":"微信锁 (福 瑞狗的蓝牙锁 默认 0 地址)","action_type":"无效"},{"dev_type":21,"name":"背景音乐(背景音乐状态)","action_type":"设备回路状态"},{"dev_type":22,"name":"房态下发","action_type":"无效"},{"dev_type":23,"name":"主机本地 调光","action_type":"无效"},{"dev_type":24,"name":"485PWM 调光( PWM 调光状态)","action_type":"无效"},{"dev_type":25,"name":"总线调光( PBLED 调光状态) - 没使用 -","action_type":"无效"},{"dev_type":26,"name":"RCU 电源","action_type":"无效"},{"dev_type":27,"name":"A9IO 开关","action_type":"用户操作"},{"dev_type":28,"name":"A9IO 扩展","action_type":"设备回路状态"},{"dev_type":29,"name":"A9IO 电源","action_type":"设备回路状态"},{"dev_type":30,"name":"无线网关轮询(用于轮询控制轮询设备;给无线网关下发配置和询问网关状态)","action_type":"无效"},{"dev_type":31,"name":"无线网关主动(用于主动控制主动设备)","action_type":"无效"},{"dev_type":32,"name":"无线门磁","action_type":"用户操作"},{"dev_type":33,"name":"空气参数显示设备","action_type":"设备回路状态"},{"dev_type":34,"name":"无线继电器红外","action_type":"设备回路状态"},{"dev_type":35,"name":"时间同步","action_type":"设备回路状态"},{"dev_type":36,"name":"监控控制","action_type":"无效"},{"dev_type":37,"name":"旋钮开关控制","action_type":"用户操作"},{"dev_type":38,"name":"CSIO - 类型","action_type":"设备回路状态"},{"dev_type":39,"name":"插卡状态虚拟设备","action_type":"设备回路状态"},{"dev_type":40,"name":"485 新风设备","action_type":"用户操作"},{"dev_type":41,"name":"485 人脸机","action_type":"用户操作"},{"dev_type":42,"name":"中控","action_type":"无效"},{"dev_type":43,"name":"域控","action_type":"无效"},{"dev_type":44,"name":"LCD","action_type":"设备回路状态"},{"dev_type":45,"name":"无卡断电 --2025-11-24 取消","action_type":"无效"},{"dev_type":46,"name":"无卡取电 2","action_type":"用户操作"},{"dev_type":47,"name":"虚拟时间设备","action_type":"设备回路状态"},{"dev_type":48,"name":"PLC 总控","action_type":"设备回路状态"},{"dev_type":49,"name":"PLC 设备 - 恒流调光设备","action_type":"设备回路状态"},{"dev_type":50,"name":"PLC 设备 - 恒压调光设备","action_type":"设备回路状态"},{"dev_type":51,"name":"PLC 设备 - 继电器设备","action_type":"设备回路状态"},{"dev_type":52,"name":"色温调节功能","action_type":"设备回路状态"},{"dev_type":53,"name":"蓝牙音频","action_type":"设备回路状态"},{"dev_type":54,"name":"碳达人","action_type":"用户操作"},{"dev_type":55,"name":"场景还原","action_type":"用户操作"},{"dev_type":56,"name":"全局设置","action_type":"设备回路状态"},{"dev_type":57,"name":"能耗检测","action_type":"设备回路状态"},{"dev_type":241,"name":"CSIO - 类型","action_type":"设备回路状态"}]'
|
||||
|
||||
113
bls-rcu-action-backend/scripts/generate_rules_from_readme.js
Normal file
113
bls-rcu-action-backend/scripts/generate_rules_from_readme.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const readmePath = path.resolve(__dirname, '../../docs/readme.md');
|
||||
const envPath = path.resolve(__dirname, '../.env');
|
||||
const processorPath = path.resolve(__dirname, '../src/processor/index.js');
|
||||
|
||||
try {
|
||||
const readmeContent = fs.readFileSync(readmePath, 'utf8');
|
||||
const lines = readmeContent.split('\n');
|
||||
|
||||
const rules = [];
|
||||
let inTable = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Detect start of table (approximately)
|
||||
if (trimmed.includes('|dev_type|名称|描述|Action Type|')) {
|
||||
inTable = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip separator line
|
||||
if (inTable && trimmed.includes('|---|')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process table rows
|
||||
if (inTable && trimmed.startsWith('|') && trimmed.endsWith('|')) {
|
||||
const parts = trimmed.split('|').map(p => p.trim());
|
||||
// parts[0] is empty, parts[1] is dev_type, parts[2] is name, parts[3] is description, parts[4] is action_type
|
||||
|
||||
if (parts.length >= 5) {
|
||||
const devTypeStr = parts[1];
|
||||
const description = parts[3];
|
||||
const actionType = parts[4];
|
||||
|
||||
if (!devTypeStr || isNaN(parseInt(devTypeStr))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const devType = parseInt(devTypeStr, 10);
|
||||
|
||||
rules.push({
|
||||
dev_type: devType,
|
||||
name: description, // Use description as name per user request
|
||||
action_type: actionType
|
||||
});
|
||||
}
|
||||
} else if (inTable && trimmed === '') {
|
||||
// Empty line might mean end of table, but let's be loose
|
||||
} else if (inTable && !trimmed.startsWith('|')) {
|
||||
// End of table
|
||||
inTable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by dev_type
|
||||
rules.sort((a, b) => a.dev_type - b.dev_type);
|
||||
|
||||
console.log(`Found ${rules.length} rules.`);
|
||||
|
||||
// 1. Generate JSON for .env
|
||||
const envJson = JSON.stringify(rules);
|
||||
|
||||
// Read existing .env
|
||||
let envContent = fs.readFileSync(envPath, 'utf8');
|
||||
const envKey = 'ACTION_TYPE_DEV_TYPE_RULES';
|
||||
|
||||
// Replace or Append
|
||||
const envLine = `${envKey}='${envJson}'`;
|
||||
const regex = new RegExp(`^${envKey}=.*`, 'm');
|
||||
|
||||
if (regex.test(envContent)) {
|
||||
envContent = envContent.replace(regex, envLine);
|
||||
} else {
|
||||
envContent += `\n${envLine}`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(envPath, envContent, 'utf8');
|
||||
console.log('Updated .env');
|
||||
|
||||
// 2. Generate Object for src/processor/index.js
|
||||
// We need to construct the object string manually to match the code style
|
||||
const mapLines = rules.map(r => {
|
||||
// Escape single quotes in name if present
|
||||
const safeName = r.name.replace(/'/g, "\\'");
|
||||
return ` ${r.dev_type}: { name: '${safeName}', action: '${r.action_type}' }`;
|
||||
});
|
||||
|
||||
const mapString = `const defaultDevTypeActionMap = {\n${mapLines.join(',\n')}\n};`;
|
||||
|
||||
let processorContent = fs.readFileSync(processorPath, 'utf8');
|
||||
|
||||
// Regex to replace the object.
|
||||
const processorRegex = /const defaultDevTypeActionMap = \{[\s\S]*?\};/m;
|
||||
|
||||
if (processorRegex.test(processorContent)) {
|
||||
processorContent = processorContent.replace(processorRegex, mapString);
|
||||
fs.writeFileSync(processorPath, processorContent, 'utf8');
|
||||
console.log('Updated src/processor/index.js');
|
||||
} else {
|
||||
console.error('Could not find defaultDevTypeActionMap in src/processor/index.js');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
}
|
||||
@@ -27,12 +27,16 @@ CREATE TABLE IF NOT EXISTS rcu_action.rcu_action_events (
|
||||
type_h SMALLINT,
|
||||
details JSONB,
|
||||
extra JSONB,
|
||||
loop_name VARCHAR(255),
|
||||
PRIMARY KEY (ts_ms, guid)
|
||||
) PARTITION BY RANGE (ts_ms);
|
||||
|
||||
ALTER TABLE rcu_action.rcu_action_events
|
||||
ADD COLUMN IF NOT EXISTS device_id VARCHAR(32) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE rcu_action.rcu_action_events
|
||||
ADD COLUMN IF NOT EXISTS loop_name VARCHAR(255);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_rcu_action_hotel_id ON rcu_action.rcu_action_events (hotel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_rcu_action_room_id ON rcu_action.rcu_action_events (room_id);
|
||||
|
||||
95
bls-rcu-action-backend/src/cache/projectMetadata.js
vendored
Normal file
95
bls-rcu-action-backend/src/cache/projectMetadata.js
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import cron from 'node-cron';
|
||||
import dbManager from '../db/databaseManager.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
class ProjectMetadataCache {
|
||||
constructor() {
|
||||
this.roomMap = new Map(); // device_id -> room_type_id
|
||||
this.loopMap = new Map(); // room_type_id:loop_address -> loop_name
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.refresh();
|
||||
} catch (error) {
|
||||
logger.error('Initial metadata refresh failed', { error: error.message });
|
||||
}
|
||||
|
||||
// Schedule 1:00 AM daily
|
||||
cron.schedule('0 1 * * *', () => {
|
||||
this.refresh().catch(err => logger.error('Scheduled metadata refresh failed', { error: err.message }));
|
||||
});
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
logger.info('Refreshing project metadata cache...');
|
||||
const client = await dbManager.pool.connect();
|
||||
try {
|
||||
// Load Rooms
|
||||
// temporary_project.rooms might be partitioned, but querying parent works.
|
||||
const roomsRes = await client.query('SELECT device_id, room_type_id FROM temporary_project.rooms');
|
||||
const newRoomMap = new Map();
|
||||
for (const row of roomsRes.rows) {
|
||||
if (row.device_id) {
|
||||
newRoomMap.set(row.device_id, row.room_type_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Loops
|
||||
const loopsRes = await client.query('SELECT room_type_id, loop_address, loop_name FROM temporary_project.loops');
|
||||
const newLoopMap = new Map();
|
||||
for (const row of loopsRes.rows) {
|
||||
if (row.room_type_id && row.loop_address) {
|
||||
// loop_address is varchar, we will key it as string
|
||||
const key = `${row.room_type_id}:${row.loop_address}`;
|
||||
newLoopMap.set(key, row.loop_name);
|
||||
}
|
||||
}
|
||||
|
||||
this.roomMap = newRoomMap;
|
||||
this.loopMap = newLoopMap;
|
||||
logger.info('Project metadata cache refreshed', {
|
||||
roomsCount: this.roomMap.size,
|
||||
loopsCount: this.loopMap.size
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// If schema/tables don't exist, this will throw.
|
||||
// We log but don't crash the app, as this is an enhancement feature.
|
||||
logger.error('Failed to refresh project metadata', { error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loop name for a given device configuration
|
||||
* @param {string} deviceId - The device ID (from room)
|
||||
* @param {number|string} devType - The device type
|
||||
* @param {number|string} devAddr - The device address
|
||||
* @param {number|string} devLoop - The device loop
|
||||
* @returns {string|null} - The loop name or null if not found
|
||||
*/
|
||||
getLoopName(deviceId, devType, devAddr, devLoop) {
|
||||
if (!deviceId ||
|
||||
devType === undefined || devType === null ||
|
||||
devAddr === undefined || devAddr === null ||
|
||||
devLoop === undefined || devLoop === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roomTypeId = this.roomMap.get(deviceId);
|
||||
if (!roomTypeId) return null;
|
||||
|
||||
// Construct loop_address: 3-digit zero-padded concatenation of type, addr, loop
|
||||
// e.g. type=1, addr=23, loop=12 -> 001023012
|
||||
const fmt = (val) => String(val).padStart(3, '0');
|
||||
const loopAddress = `${fmt(devType)}${fmt(devAddr)}${fmt(devLoop)}`;
|
||||
|
||||
const key = `${roomTypeId}:${loopAddress}`;
|
||||
return this.loopMap.get(key) || null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ProjectMetadataCache();
|
||||
@@ -28,7 +28,8 @@ const columns = [
|
||||
'type_l',
|
||||
'type_h',
|
||||
'details',
|
||||
'extra'
|
||||
'extra',
|
||||
'loop_name'
|
||||
];
|
||||
|
||||
export class DatabaseManager {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { config } from './config/config.js';
|
||||
import dbManager from './db/databaseManager.js';
|
||||
import dbInitializer from './db/initializer.js';
|
||||
import partitionManager from './db/partitionManager.js';
|
||||
import projectMetadata from './cache/projectMetadata.js';
|
||||
import { createKafkaConsumers } from './kafka/consumer.js';
|
||||
import { processKafkaMessage } from './processor/index.js';
|
||||
import { createRedisClient } from './redis/redisClient.js';
|
||||
@@ -14,6 +15,9 @@ import { logger } from './utils/logger.js';
|
||||
const bootstrap = async () => {
|
||||
// 0. Initialize Database (Create DB, Schema, Table, Partitions)
|
||||
await dbInitializer.initialize();
|
||||
|
||||
// 0.1 Initialize Project Metadata Cache
|
||||
await projectMetadata.init();
|
||||
|
||||
// Metric Collector
|
||||
const metricCollector = new MetricCollector();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createGuid } from '../utils/uuid.js';
|
||||
import { kafkaPayloadSchema } from '../schema/kafkaPayload.js';
|
||||
import projectMetadata from '../cache/projectMetadata.js';
|
||||
|
||||
const normalizeDirection = (value) => {
|
||||
if (!value) return null;
|
||||
@@ -43,116 +44,103 @@ const resolveMessageType = (direction, cmdWord) => {
|
||||
};
|
||||
|
||||
const defaultDevTypeActionMap = {
|
||||
0: '无效',
|
||||
1: '设备回路状态',
|
||||
2: '用户操作',
|
||||
3: '设备回路状态',
|
||||
4: '设备回路状态',
|
||||
5: '设备回路状态',
|
||||
6: '用户操作',
|
||||
7: '用户操作',
|
||||
8: '用户操作',
|
||||
9: '设备回路状态',
|
||||
10: '用户操作',
|
||||
11: '用户操作',
|
||||
12: '无效',
|
||||
13: '设备回路状态',
|
||||
14: '设备回路状态',
|
||||
15: '设备回路状态',
|
||||
16: '设备回路状态',
|
||||
17: '设备回路状态',
|
||||
18: '无效',
|
||||
19: '无效',
|
||||
20: '无效',
|
||||
21: '设备回路状态',
|
||||
22: '无效',
|
||||
23: '无效',
|
||||
24: '无效',
|
||||
25: '无效',
|
||||
26: '无效',
|
||||
27: '用户操作',
|
||||
28: '设备回路状态',
|
||||
29: '设备回路状态',
|
||||
30: '无效',
|
||||
31: '无效',
|
||||
32: '用户操作',
|
||||
33: '设备回路状态',
|
||||
34: '设备回路状态',
|
||||
35: '设备回路状态',
|
||||
36: '无效',
|
||||
37: '用户操作',
|
||||
38: '设备回路状态',
|
||||
39: '设备回路状态',
|
||||
40: '用户操作',
|
||||
41: '用户操作',
|
||||
42: '无效',
|
||||
43: '无效',
|
||||
44: '设备回路状态',
|
||||
45: '无效',
|
||||
46: '用户操作',
|
||||
47: '设备回路状态',
|
||||
48: '设备回路状态',
|
||||
49: '设备回路状态',
|
||||
50: '设备回路状态',
|
||||
51: '设备回路状态',
|
||||
52: '设备回路状态',
|
||||
53: '设备回路状态',
|
||||
54: '用户操作',
|
||||
55: '用户操作',
|
||||
56: '设备回路状态',
|
||||
57: '设备回路状态',
|
||||
241: '设备回路状态'
|
||||
0: { name: '无效设备(也可以被认为是场景)', action: '无效' },
|
||||
1: { name: '强电继电器(输出状态)', action: '设备回路状态' },
|
||||
2: { name: '弱电输入(输入状态)', action: '用户操作' },
|
||||
3: { name: '弱电输出(输出状态)', action: '设备回路状态' },
|
||||
4: { name: '服务信息', action: '设备回路状态' },
|
||||
5: { name: '干节点窗帘', action: '设备回路状态' },
|
||||
6: { name: '开关', action: '用户操作' },
|
||||
7: { name: '空调', action: '用户操作' },
|
||||
8: { name: '红外感应', action: '用户操作' },
|
||||
9: { name: '空气质量检测设备', action: '设备回路状态' },
|
||||
10: { name: '插卡取电', action: '用户操作' },
|
||||
11: { name: '地暖', action: '用户操作' },
|
||||
12: { name: 'RCU 设备网络 - 没使用', action: '' },
|
||||
13: { name: '窗帘', action: '设备回路状态' },
|
||||
14: { name: '继电器', action: '设备回路状态' },
|
||||
15: { name: '红外发送', action: '设备回路状态' },
|
||||
16: { name: '调光驱动', action: '设备回路状态' },
|
||||
17: { name: '可控硅调光(可控硅状态)', action: '设备回路状态' },
|
||||
18: { name: '灯带(灯带状态) --2025-11-24 取消', action: '无效' },
|
||||
19: { name: '中控', action: '无效' },
|
||||
20: { name: '微信锁 (福 瑞狗的蓝牙锁 默认 0 地址)', action: '无效' },
|
||||
21: { name: '背景音乐(背景音乐状态)', action: '设备回路状态' },
|
||||
22: { name: '房态下发', action: '无效' },
|
||||
23: { name: '主机本地 调光', action: '无效' },
|
||||
24: { name: '485PWM 调光( PWM 调光状态)', action: '无效' },
|
||||
25: { name: '总线调光( PBLED 调光状态) - 没使用 -', action: '无效' },
|
||||
26: { name: 'RCU 电源', action: '无效' },
|
||||
27: { name: 'A9IO 开关', action: '用户操作' },
|
||||
28: { name: 'A9IO 扩展', action: '设备回路状态' },
|
||||
29: { name: 'A9IO 电源', action: '设备回路状态' },
|
||||
30: { name: '无线网关轮询(用于轮询控制轮询设备;给无线网关下发配置和询问网关状态)', action: '无效' },
|
||||
31: { name: '无线网关主动(用于主动控制主动设备)', action: '无效' },
|
||||
32: { name: '无线门磁', action: '用户操作' },
|
||||
33: { name: '空气参数显示设备', action: '设备回路状态' },
|
||||
34: { name: '无线继电器红外', action: '设备回路状态' },
|
||||
35: { name: '时间同步', action: '设备回路状态' },
|
||||
36: { name: '监控控制', action: '无效' },
|
||||
37: { name: '旋钮开关控制', action: '用户操作' },
|
||||
38: { name: 'CSIO - 类型', action: '设备回路状态' },
|
||||
39: { name: '插卡状态虚拟设备', action: '设备回路状态' },
|
||||
40: { name: '485 新风设备', action: '用户操作' },
|
||||
41: { name: '485 人脸机', action: '用户操作' },
|
||||
42: { name: '中控', action: '无效' },
|
||||
43: { name: '域控', action: '无效' },
|
||||
44: { name: 'LCD', action: '设备回路状态' },
|
||||
45: { name: '无卡断电 --2025-11-24 取消', action: '无效' },
|
||||
46: { name: '无卡取电 2', action: '用户操作' },
|
||||
47: { name: '虚拟时间设备', action: '设备回路状态' },
|
||||
48: { name: 'PLC 总控', action: '设备回路状态' },
|
||||
49: { name: 'PLC 设备 - 恒流调光设备', action: '设备回路状态' },
|
||||
50: { name: 'PLC 设备 - 恒压调光设备', action: '设备回路状态' },
|
||||
51: { name: 'PLC 设备 - 继电器设备', action: '设备回路状态' },
|
||||
52: { name: '色温调节功能', action: '设备回路状态' },
|
||||
53: { name: '蓝牙音频', action: '设备回路状态' },
|
||||
54: { name: '碳达人', action: '用户操作' },
|
||||
55: { name: '场景还原', action: '用户操作' },
|
||||
56: { name: '全局设置', action: '设备回路状态' },
|
||||
57: { name: '能耗检测', action: '设备回路状态' },
|
||||
241: { name: 'CSIO - 类型', action: '设备回路状态' }
|
||||
};
|
||||
|
||||
const buildDevTypeActionMap = () => {
|
||||
const raw = process.env.ACTION_TYPE_DEV_TYPE_RULES;
|
||||
if (!raw || typeof raw !== 'string' || !raw.trim()) {
|
||||
return defaultDevTypeActionMap;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return defaultDevTypeActionMap;
|
||||
// Parse env rules if present
|
||||
let devTypeActionRules = [];
|
||||
try {
|
||||
if (process.env.ACTION_TYPE_DEV_TYPE_RULES) {
|
||||
const parsed = JSON.parse(process.env.ACTION_TYPE_DEV_TYPE_RULES);
|
||||
if (Array.isArray(parsed)) {
|
||||
devTypeActionRules = parsed;
|
||||
}
|
||||
|
||||
const overrides = {};
|
||||
parsed.forEach((item) => {
|
||||
if (Array.isArray(item) && item.length >= 2) {
|
||||
const devType = Number(item[0]);
|
||||
const actionType = item[1];
|
||||
if (Number.isFinite(devType) && typeof actionType === 'string' && actionType) {
|
||||
overrides[devType] = actionType;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (item && typeof item === 'object') {
|
||||
const devType = Number(item.dev_type ?? item.devType);
|
||||
const actionType = item.action_type ?? item.actionType ?? item.action;
|
||||
if (Number.isFinite(devType) && typeof actionType === 'string' && actionType) {
|
||||
overrides[devType] = actionType;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ...defaultDevTypeActionMap, ...overrides };
|
||||
} catch {
|
||||
return defaultDevTypeActionMap;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silent fallback
|
||||
}
|
||||
|
||||
const getActionTypeByDevType = (devType) => {
|
||||
// 1. Env override
|
||||
const rule = devTypeActionRules.find(r => r.dev_type === devType);
|
||||
if (rule?.action_type) return rule.action_type;
|
||||
|
||||
// 2. Default map
|
||||
const entry = defaultDevTypeActionMap[devType];
|
||||
return entry?.action || '设备回路状态';
|
||||
};
|
||||
|
||||
const devTypeActionMap = buildDevTypeActionMap();
|
||||
const getDevTypeName = (devType) => {
|
||||
// 1. Env override
|
||||
const rule = devTypeActionRules.find(r => r.dev_type === devType);
|
||||
if (rule?.name) return rule.name;
|
||||
|
||||
// 2. Default map
|
||||
const entry = defaultDevTypeActionMap[devType];
|
||||
return entry?.name || 'Unknown';
|
||||
};
|
||||
|
||||
const resolveDevTypeAction = (devType) => {
|
||||
if (typeof devType !== 'number') {
|
||||
return '设备回路状态';
|
||||
}
|
||||
const mapped = devTypeActionMap[devType];
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
return '设备回路状态';
|
||||
if (devType === null || devType === undefined) return '设备回路状态';
|
||||
return getActionTypeByDevType(devType);
|
||||
};
|
||||
|
||||
const parseKafkaPayload = (value) => {
|
||||
@@ -247,6 +235,19 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
extra: extra || {}
|
||||
};
|
||||
|
||||
// Helper to generate loop name if not found in cache
|
||||
const getLoopNameWithFallback = (deviceId, devType, devAddr, devLoop) => {
|
||||
// 1. Try cache
|
||||
const cachedName = projectMetadata.getLoopName(deviceId, devType, devAddr, devLoop);
|
||||
if (cachedName) return cachedName;
|
||||
|
||||
// 2. Fallback: [TypeName-Addr-Loop]
|
||||
const typeName = getDevTypeName(devType);
|
||||
if (!typeName) return null; // Should have a name if devType is valid
|
||||
|
||||
return `[${devType}${typeName}-${devAddr}-${devLoop}]`;
|
||||
};
|
||||
|
||||
const rows = [];
|
||||
|
||||
// Logic 1: 0x36 Status/Fault Report
|
||||
@@ -268,6 +269,7 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
dev_loop: device.dev_loop ?? null,
|
||||
dev_data: device.dev_data ?? null,
|
||||
action_type: actionType,
|
||||
loop_name: getLoopNameWithFallback(deviceId, device.dev_type, device.dev_addr, device.dev_loop),
|
||||
details
|
||||
});
|
||||
});
|
||||
@@ -287,6 +289,7 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
error_type: fault.error_type ?? null,
|
||||
error_data: fault.error_data ?? null,
|
||||
action_type: actionType,
|
||||
loop_name: getLoopNameWithFallback(deviceId, fault.dev_type, fault.dev_addr, fault.dev_loop),
|
||||
details
|
||||
});
|
||||
});
|
||||
@@ -322,6 +325,7 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
type_l: control.type_l ?? null,
|
||||
type_h: control.type_h ?? null,
|
||||
action_type: '下发控制',
|
||||
loop_name: getLoopNameWithFallback(deviceId, control.dev_type, control.dev_addr, control.dev_loop),
|
||||
details
|
||||
});
|
||||
});
|
||||
@@ -340,18 +344,29 @@ export const buildRowsFromPayload = (rawPayload) => {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Logic 3: 0x0F ACK or others
|
||||
const fallbackActionType =
|
||||
normalizedCmdWord === '0x0f' && normalizedDirection === '上报'
|
||||
? 'ACK'
|
||||
: '无效';
|
||||
// 3. 0x0F ACK
|
||||
else if (messageType === '0FACK') {
|
||||
const { control_list: controls = [] } = payload;
|
||||
if (Array.isArray(controls)) {
|
||||
const details = { control_list: controls };
|
||||
controls.forEach((control) => {
|
||||
rows.push({
|
||||
...commonFields,
|
||||
guid: createGuid(),
|
||||
dev_type: control.dev_type ?? null,
|
||||
dev_addr: control.dev_addr ?? null,
|
||||
dev_loop: control.dev_loop ?? null,
|
||||
dev_data: control.dev_data ?? null,
|
||||
type_h: control.type_h ?? null,
|
||||
action_type: '设备回路状态',
|
||||
loop_name: getLoopNameWithFallback(deviceId, control.dev_type, control.dev_addr, control.dev_loop),
|
||||
details
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
...commonFields,
|
||||
guid: createGuid(),
|
||||
action_type: fallbackActionType,
|
||||
details: {}
|
||||
}];
|
||||
return rows;
|
||||
};
|
||||
|
||||
export const processKafkaMessage = async ({ message, dbManager, config }) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildRowsFromPayload } from '../src/processor/index.js';
|
||||
import projectMetadata from '../src/cache/projectMetadata.js';
|
||||
|
||||
describe('Processor Logic', () => {
|
||||
const basePayload = {
|
||||
@@ -95,12 +96,15 @@ describe('Processor Logic', () => {
|
||||
const payload = {
|
||||
...basePayload,
|
||||
direction: '上报',
|
||||
cmd_word: '0x0F'
|
||||
cmd_word: '0x0F',
|
||||
control_list: [
|
||||
{ dev_type: 1, dev_addr: 1, dev_loop: 1, dev_data: 1, type_h: 0 }
|
||||
]
|
||||
};
|
||||
|
||||
const rows = buildRowsFromPayload(payload);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].action_type).toBe('ACK');
|
||||
expect(rows[0].action_type).toBe('设备回路状态');
|
||||
});
|
||||
|
||||
it('should fallback when lists are empty for 0x36', () => {
|
||||
@@ -199,4 +203,27 @@ describe('Processor Logic', () => {
|
||||
trace_id: 'trace-123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should enrich rows with loop_name from metadata', () => {
|
||||
// Mock metadata
|
||||
projectMetadata.roomMap.set('dev_001', 101);
|
||||
// Key format: roomTypeId:00Type00Addr00Loop
|
||||
// type=1, addr=10, loop=1 -> 001010001
|
||||
projectMetadata.loopMap.set('101:001010001', 'Main Chandelier');
|
||||
|
||||
const payload = {
|
||||
...basePayload,
|
||||
direction: '上报',
|
||||
cmd_word: '0x36',
|
||||
device_list: [
|
||||
{ dev_type: 1, dev_addr: 10, dev_loop: 1, dev_data: 100 }, // Should match 001010001
|
||||
{ dev_type: 1, dev_addr: 10, dev_loop: 2, dev_data: 0 } // Should not match (001010002) -> Fallback
|
||||
]
|
||||
};
|
||||
|
||||
const rows = buildRowsFromPayload(payload);
|
||||
expect(rows[0].loop_name).toBe('Main Chandelier');
|
||||
// dev_type 1 is 'Dev_Host_HVout'
|
||||
expect(rows[1].loop_name).toBe('[1Dev_Host_HVout-10-2]');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user