| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- 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<IPMonitorResult>((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))
- };
- }
- }
|