import { APIClient } from '@d8d-appcontainer/api' import debug from "debug" import { ipMonitor, type IPMonitorResult } from './ip_monitor.ts'; import { readModbusRTU } from './modbus_rtu.ts'; import { Context } from "hono"; import { SmsController } from "../controllers/sms.ts"; import type { DeviceAlert, DeviceMonitorData } from '../../client/share/monitorTypes.ts'; import { DeviceCategory, DeviceStatus, AlertLevel, AlertStatus, NotifyType, DeviceProtocolType, MetricType, NetworkStatus, PacketLossStatus } from '../../client/share/monitorTypes.ts'; import { EnableStatus, DeleteStatus, } from '../../client/share/types.ts'; const log = { app:debug('app:monitor'), debug:debug('debug:monitor') }; // 设备监控功能 export const startMonitoring = (apiClient: APIClient) => { log.app('开始设备监控...'); // 传感器类型轮巡配置 const sensorCycles = [ { type: MetricType.TEMPERATURE, interval: 30000 }, // 温度每30秒 { type: MetricType.HUMIDITY, interval: 30000 }, // 湿度每30秒 { type: 'smoke', interval: 60000 }, // 烟感每分钟 { type: 'water', interval: 60000 } // 水浸每分钟 ]; // 启动每种传感器的轮巡定时器 sensorCycles.forEach((sensor) => { setInterval(async () => { try { // 获取需要监控的设备列表,联合查询设备实例和资产信息 const devices = await apiClient.database .table('device_instances as di') .leftJoin('zichan_info as zi', 'di.id', 'zi.id') .select('di.*', 'zi.asset_name', 'zi.device_status', 'zi.device_category') .where('di.is_enabled', EnableStatus.ENABLED) .where('di.is_deleted', DeleteStatus.NOT_DELETED) .where('zi.is_deleted', DeleteStatus.NOT_DELETED); if (devices.length === 0) { log.app('没有找到可监控的设备实例'); return; } log.app(`找到 ${devices.length} 个可监控的设备实例`); // 使用Promise.all并发处理所有设备 await Promise.all(devices.map(async (device) => { try { // 更新设备的最后采集时间 await apiClient.database.table('device_instances') .where('id', device.id) .update({ last_collect_time: apiClient.database.fn.now(), updated_at: apiClient.database.fn.now() }); // 生成当前传感器类型的监控数据 const monitorData = await generateMonitorData(device, sensor.type); if (!monitorData || monitorData.length === 0) { log.debug(`设备 ${device.id} 未生成监控数据`); return; } // 并发插入监控数据 await Promise.all(monitorData.map(async (data) => { try { const [insertId] = await apiClient.database.table('device_monitor_data').insert({ ...data, collect_time: apiClient.database.fn.now(), created_at: apiClient.database.fn.now(), updated_at: apiClient.database.fn.now() }); const insertedData = await apiClient.database.table('device_monitor_data') .select('*') .where('id', insertId) .first(); const updateData:{network_status?: NetworkStatus, packet_loss?: PacketLossStatus} = {}; if(data.metric_type === MetricType.NETWORK_TRAFFIC) { updateData.network_status = data.status === DeviceStatus.NORMAL ? NetworkStatus.CONNECTED : NetworkStatus.DISCONNECTED; } if(data.metric_type === MetricType.PACKET_LOSS) { updateData.packet_loss = data.status === DeviceStatus.NORMAL ? PacketLossStatus.NORMAL : PacketLossStatus.HIGH; } // 根据监控数据指标更新设备状态 await apiClient.database.table('zichan_info') .where('id', device.id) .update({ device_status: insertedData.status, ...updateData }); // 检查是否触发告警 if (insertedData) { await checkAndTriggerAlert(apiClient, insertedData); } } catch (error) { log.app(`监控数据 ${data.metric_type} 插入失败:`, error); } })); } catch (deviceError) { log.app(`设备 ${device.id} 处理失败:`, deviceError); } })); } catch (error) { log.app('监控数据收集失败:', error); } }, sensor.interval); }); }; // 生成指定类型的监控数据 export const generateMonitorData = async (device: { id: number; device_category?: DeviceCategory; asset_name?: string; device_status?: DeviceStatus; address?: string; protocol?: DeviceProtocolType; }, metricType: string) => { const data = []; const deviceCategory = device.device_category; const protocol = device.protocol; // 根据传感器类型生成特定数据 switch (metricType) { case MetricType.TEMPERATURE: case MetricType.HUMIDITY: // 温湿度传感器数据 if (protocol === DeviceProtocolType.MODBUS && device.address) { // 实际从MODBUS设备读取数据 const result = await readModbusSensorData(device.address, metricType); if (result) { data.push({ device_id: device.id, metric_type: metricType, metric_value: result.value, unit: result.unit, status: DeviceStatus.NORMAL }); } } else { // 模拟数据 data.push({ device_id: device.id, metric_type: metricType, metric_value: metricType === MetricType.TEMPERATURE ? 20 + Math.random() * 15 : 40 + Math.random() * 40, unit: metricType === MetricType.TEMPERATURE ? '°C' : '%', status: DeviceStatus.NORMAL }); } break; case 'smoke': // 烟感传感器数据 data.push({ device_id: device.id, metric_type: 'smoke', metric_value: Math.random() > 0.95 ? 1 : 0, // 5%概率触发 unit: '', status: Math.random() > 0.95 ? DeviceStatus.FAULT : DeviceStatus.NORMAL }); break; case 'water': // 水浸传感器数据 data.push({ device_id: device.id, metric_type: 'water', metric_value: Math.random() > 0.9 ? 1 : 0, // 10%概率触发 unit: '', status: Math.random() > 0.9 ? DeviceStatus.FAULT : DeviceStatus.NORMAL }); break; } // // 根据设备类型生成不同的监控数据 // if (deviceCategory === DeviceCategory.SERVER) { // // CPU使用率 // data.push({ // device_id: device.id, // metric_type: MetricType.CPU_USAGE, // metric_value: Math.floor(Math.random() * 100), // unit: '%', // status: DeviceStatus.NORMAL // }); // // 内存使用率 // data.push({ // device_id: device.id, // metric_type: MetricType.MEMORY_USAGE, // metric_value: Math.floor(Math.random() * 100), // unit: '%', // status: DeviceStatus.NORMAL // }); // // 磁盘使用率 // data.push({ // device_id: device.id, // metric_type: MetricType.DISK_USAGE, // metric_value: Math.floor(Math.random() * 90), // unit: '%', // status: DeviceStatus.NORMAL // }); // } // // 温度 - 所有设备类型 // data.push({ // device_id: device.id, // metric_type: MetricType.TEMPERATURE, // metric_value: 20 + Math.random() * 15, // unit: '°C', // status: DeviceStatus.NORMAL // }); // // 湿度 - 所有设备类型 // data.push({ // device_id: device.id, // metric_type: MetricType.HUMIDITY, // metric_value: 40 + Math.random() * 40, // unit: '%', // status: DeviceStatus.NORMAL // }); // // 网络流量 - 所有设备类型 // data.push({ // device_id: device.id, // metric_type: MetricType.NETWORK_TRAFFIC, // metric_value: Math.random() * 1000, // unit: 'MB/s', // status: DeviceStatus.NORMAL // }); // IP地址连通性检测 - 根据不同协议生成不同的监控数据 if (protocol === DeviceProtocolType.TCP && device.address) { // 使用真实的 IP 监控数据 const ipResult = await new Promise((resolve) => { ipMonitor.startMonitor(device.address!, (result) => { resolve(result); }); }); // 根据协议类型添加特定的监控数据 // switch (protocol) { // case DeviceProtocolType.SNMP: { // // SNMP协议设备 - 添加SNMP特有的监控指标 // const snmpResponseTime = Math.floor(10 + Math.random() * 90); // data.push({ // device_id: device.id, // metric_type: MetricType.SNMP_RESPONSE_TIME, // metric_value: snmpResponseTime, // unit: 'ms', // status: snmpResponseTime > 80 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL // }); // // SNMP错误计数 // const snmpErrors = Math.floor(Math.random() * 5); // data.push({ // device_id: device.id, // metric_type: MetricType.SNMP_ERRORS, // metric_value: snmpErrors, // unit: '', // status: snmpErrors > 2 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL // }); // break; // } // case DeviceProtocolType.HTTP: { // // HTTP协议设备 - 添加HTTP特有的监控指标 // // HTTP响应时间 // const httpResponseTime = Math.floor(20 + Math.random() * 200); // data.push({ // device_id: device.id, // metric_type: MetricType.HTTP_RESPONSE_TIME, // metric_value: httpResponseTime, // unit: 'ms', // status: httpResponseTime > 180 ? DeviceStatus.FAULT : // httpResponseTime > 100 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL // }); // // HTTP状态码 // const httpStatus = Math.random() > 0.9 ? 500 : // Math.random() > 0.95 ? 404 : 200; // data.push({ // device_id: device.id, // metric_type: MetricType.HTTP_STATUS, // metric_value: httpStatus, // unit: '', // status: httpStatus !== 200 ? DeviceStatus.FAULT : DeviceStatus.NORMAL // }); // break; // } // case DeviceProtocolType.TCP: { // // TCP协议设备 // // TCP连接时间 // const tcpConnTime = Math.floor(5 + Math.random() * 45); // data.push({ // device_id: device.id, // metric_type: MetricType.TCP_CONNECTION_TIME, // metric_value: tcpConnTime, // unit: 'ms', // status: tcpConnTime > 30 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL // }); // break; // } // } // 添加 IP 监控数据 if (ipResult.success) { data.push({ device_id: device.id, metric_type: MetricType.PING_TIME, metric_value: ipResult.responseTime || 0, unit: 'ms', status: ipResult.responseTime && ipResult.responseTime > 150 ? DeviceStatus.FAULT : ipResult.responseTime && ipResult.responseTime > 100 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL }); data.push({ device_id: device.id, metric_type: MetricType.PACKET_LOSS, metric_value: ipResult.packetLoss || 0, unit: '%', status: ipResult.packetLoss && ipResult.packetLoss > 5 ? DeviceStatus.FAULT : ipResult.packetLoss && ipResult.packetLoss > 2 ? DeviceStatus.MAINTAIN : DeviceStatus.NORMAL }); data.push({ device_id: device.id, metric_type: MetricType.CONNECTION_STATUS, metric_value: ipResult.success ? NetworkStatus.CONNECTED : NetworkStatus.DISCONNECTED, unit: '', status: ipResult.success ? DeviceStatus.NORMAL : DeviceStatus.FAULT }); } else { // IP 监控失败 data.push({ device_id: device.id, metric_type: MetricType.CONNECTION_STATUS, metric_value: NetworkStatus.DISCONNECTED, unit: '', status: DeviceStatus.FAULT }); } } return data; }; // 检查是否触发告警 export const checkAndTriggerAlert = async (apiClient: APIClient, monitorData: DeviceMonitorData) => { try { // 获取设备告警规则 const rules = await apiClient.database.table('device_alert_rules') .select('*') .where('device_id', monitorData.device_id) .where('metric_type', monitorData.metric_type) .where('is_enabled', EnableStatus.ENABLED) .where('is_deleted', DeleteStatus.NOT_DELETED); if (!rules || rules.length === 0) { log.app(`未找到设备ID: ${monitorData.device_id} 的告警规则`); return; } // 获取设备信息 - 联合查询设备实例和资产信息 const deviceInfo = await apiClient.database .table('device_instances as di') .leftJoin('zichan_info as zi', 'di.id', 'zi.id') .select('di.*', 'zi.asset_name', 'zi.device_category') .where('di.id', monitorData.device_id) .first(); if (!deviceInfo) { log.app(`未找到设备ID: ${monitorData.device_id} 的信息`); return; } // 检查每条规则 for (const rule of rules) { let isTriggered = false; // 确保 最小值 和 最大值 是数字 if (rule.min_value !== null && typeof rule.min_value !== 'number') { rule.min_value = Number(rule.min_value); } if (rule.max_value !== null && typeof rule.max_value !== 'number') { rule.max_value = Number(rule.max_value); } // 检查最小值 if (rule.min_value !== null && monitorData.metric_value < rule.min_value) { isTriggered = true; } // 检查最大值 if (rule.max_value !== null && monitorData.metric_value > rule.max_value) { isTriggered = true; } if (isTriggered) { // 替换告警消息模板中的变量 let alertMessage = rule.alert_message || `设备${deviceInfo.asset_name || deviceInfo.id}的${monitorData.metric_type}值异常: ${monitorData.metric_value}${monitorData.unit || ''}`; // 替换模板变量 alertMessage = alertMessage .replace(/\{\{device_name\}\}/g, deviceInfo.asset_name || `设备${deviceInfo.id}`) .replace(/\{\{metric_value\}\}/g, monitorData.metric_value.toString()) .replace(/\{\{unit\}\}/g, monitorData.unit || ''); // 创建告警记录 const [alertId] = await apiClient.database.table('device_alerts').insert({ device_id: monitorData.device_id, device_name: deviceInfo.asset_name || `设备${deviceInfo.id}`, metric_type: monitorData.metric_type, metric_value: monitorData.metric_value, alert_level: rule.alert_level, alert_message: alertMessage, status: AlertStatus.PENDING, created_at: apiClient.database.fn.now(), updated_at: apiClient.database.fn.now() }); const alert = await apiClient.database.table('device_alerts') .select('*') .where('id', alertId) .first(); if (alert) { log.app(`触发告警: ${alertMessage}`); // 发送告警通知 await sendAlertNotification(apiClient, alert); } } } } catch (error) { log.app('检查告警失败:', error); } }; // 发送告警通知 export const sendAlertNotification = async (apiClient: APIClient, alert: DeviceAlert) => { try { // 查询告警通知配置 const notifyConfigs = await apiClient.database.table('alert_notify_configs') .select('*') .where('device_id', alert.device_id) .where('alert_level', alert.alert_level) .where('is_enabled', EnableStatus.ENABLED) .where('is_deleted', DeleteStatus.NOT_DELETED); if (!notifyConfigs || notifyConfigs.length === 0) { log.app(`设备 ${alert.device_id} 没有配置告警级别 ${alert.alert_level} 的通知`); return; } // 对每个通知配置进行处理 for (const config of notifyConfigs) { log.debug('通知配置 %O',config) // 解析通知用户列表 const notifyUsers = config.notify_users || []; if (notifyUsers.length === 0) { log.app(`设备 ${alert.device_id} 的通知配置 ${config.id} 没有指定通知用户`); continue; } // 查询用户信息 const users = await apiClient.database.table('users') .select('*') .whereIn('id', notifyUsers); if (!users || users.length === 0) { log.app(`找不到通知配置 ${config.id} 指定的用户`); continue; } // 替换通知模板变量 let notifyContent = config.notify_template || `告警: ${alert.alert_message}`; notifyContent = notifyContent .replace(/\{\{alert_message\}\}/g, alert.alert_message) .replace(/\{\{alert_level\}\}/g, AlertLevel[alert.alert_level] || '未知'); // 根据通知类型发送通知 switch (config.notify_type) { case NotifyType.SMS: // 实现短信通知 for (const user of users) { if (user.phone) { try { const smsContent = `【告警通知】 设备ID: ${alert.device_id} 告警级别: ${AlertLevel[alert.alert_level] || '未知'} 告警时间: ${new Date(alert.created_at).toLocaleString()} 告警内容: ${alert.alert_message}`; const mockCtx = { req: { json: async () => ({ phone: user.phone, content: smsContent }) } } as Context; await SmsController.sendSms(mockCtx); log.app(`成功发送短信通知给用户 ${user.username}(${user.phone})`); } catch (error) { log.app(`发送短信通知给用户 ${user.username}(${user.phone}) 失败:`, error); } } else { log.app(`用户 ${user.username} 没有配置手机号,无法发送短信通知`); } } break; case NotifyType.EMAIL: // 实现邮件通知 for (const user of users) { if (user.email) { log.app(`向用户 ${user.username}(${user.email}) 发送邮件通知: ${notifyContent}`); // 实际环境中调用邮件发送API } else { log.app(`用户 ${user.username} 没有配置邮箱,无法发送邮件通知`); } } break; case NotifyType.WECHAT: // 实现微信通知 for (const user of users) { log.app(`向用户 ${user.username} 发送微信通知: ${notifyContent}`); // 实际环境中调用微信发送API } break; default: log.app(`不支持的通知类型: ${config.notify_type}`); break; } } } catch (error) { log.app('发送告警通知失败:', error); } }; // 从MODBUS设备读取传感器数据 async function readModbusSensorData( address: string, metricType: string ): Promise<{ value: number; unit: string } | null> { try { // 根据传感器类型设置不同的寄存器地址 let registerAddress = 0; switch (metricType) { case MetricType.TEMPERATURE: registerAddress = 0x1000; // 温度寄存器地址 break; case MetricType.HUMIDITY: registerAddress = 0x1002; // 湿度寄存器地址 break; case 'smoke': registerAddress = 0x1004; // 烟感寄存器地址 break; case 'water': registerAddress = 0x1006; // 水浸寄存器地址 break; } // 调用modbus_rtu.ts中的方法读取数据 const result = await readModbusRTU(address, registerAddress, 2); // 解析返回的数据 if (result && result.length >= 2) { const value = (result[0] << 8) | result[1]; // 组合高低字节 return { value: metricType === 'smoke' || metricType === 'water' ? value > 0 ? 1 : 0 // 烟感/水浸为开关量 : value / 10, // 温湿度为模拟量,除以10得到实际值 unit: metricType === MetricType.TEMPERATURE ? '°C' : metricType === MetricType.HUMIDITY ? '%' : '' }; } return null; } catch (error) { log.app(`读取MODBUS传感器数据失败: ${error}`); return null; } } // 设备监控功能 /** * 解析温湿度传感器十六进制数据 * @param hexData 十六进制数据字符串,格式如"[02 04 04 01 02 02 65 A9 F3]" * @returns { temperature: number, humidity: number } 解析后的温湿度对象 * @throws 如果数据格式无效会抛出错误 */ export function parseTemperatureHumidity(hexData: string): { temperature: { value: number; unit: string }; humidity: { value: number; unit: string }; error?: string; } { try { // 验证数据格式 if (!/^\[\s*(?:[0-9A-Fa-f]{2}\s*)+\]$/.test(hexData)) { return { temperature: { value: 0, unit: '°C' }, humidity: { value: 0, unit: '%' }, error: '数据格式错误: 必须以方括号开头和结尾' }; } // 提取十六进制字节数组 const bytes = hexData .replace(/[\[\]\s]/g, '') .match(/.{1,2}/g) ?.map(byte => parseInt(byte, 16)) || []; if (bytes.length < 7) { return { temperature: { value: 0, unit: '°C' }, humidity: { value: 0, unit: '%' }, error: '数据长度不足: 至少需要7个十六进制字节' }; } // 解析温度值 (01位) const tempInt = bytes[3]; // 整数部分 const tempFrac = bytes[4]; // 小数部分 const temperature = parseFloat(`${tempInt}.${tempFrac}`); // 解析湿度值 (02位) const humidityInt = bytes[5]; // 整数部分 const humidityFrac = bytes[6]; // 小数部分 const humidity = parseFloat(`${humidityInt}.${humidityFrac}`); return { temperature: { value: temperature, unit: '°C' }, humidity: { value: humidity, unit: '%' } }; } catch (error) { return { temperature: { value: 0, unit: '°C' }, humidity: { value: 0, unit: '%' }, error: '数据解析失败: ' + (error instanceof Error ? error.message : String(error)) }; } }