/** * FarmBox IoT Bridge * MQTT → InfluxDB + farm-cmms * * Топики MQTT: * farm/sensors/{device_id} — данные с датчиков * farm/lora/{device_id} — данные с LoRa датчиков * farm/status/{device_id} — статус устройства * * Пример payload: * {"id":"uvv_01","mh":1247,"temp":68.5,"current":8.2,"vibr":0.8} */ const mqtt = require('mqtt'); const { InfluxDB, Point } = require('@influxdata/influxdb-client'); const fetch = require('node-fetch'); // ─── Конфиг ─────────────────────────────────────────── const MQTT_HOST = process.env.MQTT_HOST || 'localhost'; const MQTT_PORT = process.env.MQTT_PORT || 1883; const INFLUX_URL = process.env.INFLUX_URL || 'http://localhost:8086'; const INFLUX_TOKEN = process.env.INFLUX_TOKEN; const INFLUX_ORG = process.env.INFLUX_ORG || 'farm'; const INFLUX_BUCKET= process.env.INFLUX_BUCKET || 'sensors'; const CMMS_URL = process.env.FARM_CMMS_URL || 'http://localhost:3005'; const CMMS_TOKEN = process.env.FARM_CMMS_TOKEN; // ─── Правила автоматизации ───────────────────────────── // Если значение превышает порог → автоматически создаётся заявка в farm-cmms const ALERT_RULES = [ { field: 'temp', operator: '>', threshold: 85, severity: 'high', title: (d) => `Перегрев подшипника — ${d.name || d.id}`, description: (d, val) => `Температура подшипника: ${val}°C (норма < 85°C). Требуется осмотр.` }, { field: 'temp', operator: '>', threshold: 70, severity: 'medium', title: (d) => `Повышенная температура — ${d.name || d.id}`, description: (d, val) => `Температура: ${val}°C. Рекомендуется проверить смазку.` }, { field: 'current', operator: '>', threshold: 10, severity: 'high', title: (d) => `Перегрузка двигателя — ${d.name || d.id}`, description: (d, val) => `Ток двигателя: ${val}A (норма < 10A). Возможна механическая перегрузка.` }, { field: 'vibr', operator: '>', threshold: 2.0, severity: 'medium', title: (d) => `Повышенная вибрация — ${d.name || d.id}`, description: (d, val) => `Вибрация: ${val} мм/с (норма < 2.0). Проверить ремённую передачу.` } ]; // Пороги моточасов для автосоздания задач ТО const MOTOHOUR_RULES = [ { hours: 250, task: 'Проверка натяжения ремня и уровня масла' }, { hours: 500, task: 'ТО-500ч: замена масла, ревизия уплотнений' }, { hours: 1000, task: 'ТО-1000ч: полная ревизия, замена подшипников' }, { hours: 2000, task: 'ТО-2000ч: капитальный осмотр' }, ]; // ─── InfluxDB клиент ─────────────────────────────────── const influx = new InfluxDB({ url: INFLUX_URL, token: INFLUX_TOKEN }); const writeApi = influx.getWriteApi(INFLUX_ORG, INFLUX_BUCKET, 'ms'); // ─── Состояние (кэш последних значений) ─────────────── const deviceState = {}; // последние значения по устройству const alertCooldown = {}; // не спамить заявками: device+field → timestamp const COOLDOWN_MS = 30 * 60 * 1000; // 30 минут между одинаковыми заявками // ─── MQTT подключение ────────────────────────────────── const client = mqtt.connect(`mqtt://${MQTT_HOST}:${MQTT_PORT}`, { clientId: 'farmbox-bridge', reconnectPeriod: 5000, keepalive: 60, }); client.on('connect', () => { console.log(`[Bridge] ✅ MQTT подключён к ${MQTT_HOST}:${MQTT_PORT}`); client.subscribe('farm/sensors/#'); client.subscribe('farm/lora/#'); client.subscribe('farm/status/#'); console.log('[Bridge] Слушаю топики: farm/sensors/#, farm/lora/#, farm/status/#'); }); client.on('error', (err) => console.error('[Bridge] MQTT ошибка:', err.message)); client.on('offline', () => console.warn('[Bridge] MQTT оффлайн, переподключение...')); // ─── Обработка входящих сообщений ───────────────────── client.on('message', async (topic, buffer) => { let payload; try { payload = JSON.parse(buffer.toString()); } catch { console.warn(`[Bridge] Невалидный JSON из топика ${topic}`); return; } const parts = topic.split('/'); const type = parts[1]; // sensors / lora / status const deviceId = parts[2]; // uvv_01, temp_barn1, etc. if (!deviceId) return; console.log(`[Bridge] 📡 ${topic}:`, payload); // Обновляем состояние устройства deviceState[deviceId] = { ...deviceState[deviceId], ...payload, lastSeen: Date.now() }; // Записываем в InfluxDB await writeToInflux(deviceId, type, payload); // Проверяем правила алертов await checkAlertRules(deviceId, payload); // Обновляем моточасы в farm-cmms if (payload.mh !== undefined) { await updateMotohours(deviceId, payload.mh); } // Обновляем показания воды if (payload.water !== undefined) { await updateWaterReading(deviceId, payload.water); } }); // ─── Запись в InfluxDB ───────────────────────────────── async function writeToInflux(deviceId, type, data) { try { const point = new Point('sensor_data') .tag('device_id', deviceId) .tag('device_type', type) .tag('equipment_id', data.eq_id || 'unknown'); if (data.temp !== undefined) point.floatField('temperature', data.temp); if (data.current !== undefined) point.floatField('current', data.current); if (data.vibr !== undefined) point.floatField('vibration', data.vibr); if (data.mh !== undefined) point.intField('motohours', data.mh); if (data.water !== undefined) point.floatField('water_flow', data.water); if (data.pressure !== undefined) point.floatField('pressure', data.pressure); if (data.humidity !== undefined) point.floatField('humidity', data.humidity); if (data.rssi !== undefined) point.intField('rssi', data.rssi); if (data.battery !== undefined) point.intField('battery', data.battery); writeApi.writePoint(point); await writeApi.flush(); } catch (err) { console.error('[Bridge] InfluxDB ошибка:', err.message); } } // ─── Проверка правил алертов → создание заявок ──────── async function checkAlertRules(deviceId, data) { for (const rule of ALERT_RULES) { const val = data[rule.field]; if (val === undefined) continue; const triggered = rule.operator === '>' ? val > rule.threshold : val < rule.threshold; if (!triggered) continue; // Кулдаун — не спамить одинаковыми заявками const cooldownKey = `${deviceId}:${rule.field}:${rule.threshold}`; const lastAlert = alertCooldown[cooldownKey] || 0; if (Date.now() - lastAlert < COOLDOWN_MS) continue; alertCooldown[cooldownKey] = Date.now(); const device = deviceState[deviceId] || {}; await createEmergencyRequest({ title: rule.title(device), description: rule.description(device, val), severity: rule.severity, equipment_id: data.eq_id, source: 'iot_sensor', sensor_data: { device_id: deviceId, field: rule.field, value: val } }); } } // ─── Обновление моточасов в farm-cmms ───────────────── async function updateMotohours(deviceId, hours) { try { const eqId = deviceIdToEquipmentId(deviceId); if (!eqId) return; await apiFetch(`/api/iot/motohours`, 'POST', { equipment_id: eqId, device_id: deviceId, motohours: hours, timestamp: new Date().toISOString() }); // Проверяем пороги ТО for (const rule of MOTOHOUR_RULES) { const prev = deviceState[deviceId]?.mh_prev || 0; if (prev < rule.hours && hours >= rule.hours) { await createMaintenanceTask(eqId, rule.task, hours); } } if (deviceState[deviceId]) { deviceState[deviceId].mh_prev = hours; } } catch (err) { console.error('[Bridge] updateMotohours ошибка:', err.message); } } // ─── Создание аварийной заявки в farm-cmms ───────────── async function createEmergencyRequest(data) { try { await apiFetch('/api/iot/emergency', 'POST', data); console.log(`[Bridge] 🚨 Аварийная заявка создана: ${data.title}`); } catch (err) { console.error('[Bridge] Ошибка создания заявки:', err.message); } } // ─── Создание задачи ТО в farm-cmms ─────────────────── async function createMaintenanceTask(equipmentId, taskTitle, motohours) { try { await apiFetch('/api/iot/task', 'POST', { equipment_id: equipmentId, title: taskTitle, description: `Автоматически создано при достижении ${motohours} моточасов`, source: 'motohour_trigger', motohours }); console.log(`[Bridge] 📋 Задача ТО создана: ${taskTitle} (${motohours}ч)`); } catch (err) { console.error('[Bridge] Ошибка создания задачи ТО:', err.message); } } // ─── Обновление показаний воды ───────────────────────── async function updateWaterReading(deviceId, value) { try { await apiFetch('/api/iot/water', 'POST', { device_id: deviceId, value, timestamp: new Date().toISOString() }); } catch (err) { console.error('[Bridge] updateWaterReading ошибка:', err.message); } } // ─── Вспомогательные функции ────────────────────────── function deviceIdToEquipmentId(deviceId) { // Маппинг device_id → equipment_id в farm-cmms // Настраивается при установке const mapping = JSON.parse(process.env.DEVICE_MAPPING || '{}'); return mapping[deviceId] || null; } async function apiFetch(path, method, body) { const res = await fetch(`${CMMS_URL}${path}`, { method, headers: { 'Content-Type': 'application/json', 'X-IoT-Token': CMMS_TOKEN }, body: JSON.stringify(body) }); if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); return res.json(); } // ─── Мониторинг оффлайн датчиков ────────────────────── setInterval(() => { const now = Date.now(); const OFFLINE_THRESHOLD = 10 * 60 * 1000; // 10 минут без пакетов for (const [deviceId, state] of Object.entries(deviceState)) { if (now - state.lastSeen > OFFLINE_THRESHOLD) { const cooldownKey = `${deviceId}:offline`; const lastAlert = alertCooldown[cooldownKey] || 0; if (now - lastAlert > COOLDOWN_MS) { alertCooldown[cooldownKey] = now; console.warn(`[Bridge] ⚠️ Нет связи с датчиком ${deviceId}`); createEmergencyRequest({ title: `Нет связи с датчиком ${deviceId}`, description: `Датчик не отправлял данные более 10 минут. Проверьте питание и связь.`, severity: 'medium', equipment_id: deviceState[deviceId]?.eq_id, source: 'connectivity_monitor' }); } } } }, 60 * 1000); // проверяем каждую минуту console.log('[Bridge] 🚀 FarmBox IoT Bridge запущен');