// server.js const http = require('http'); const { Server } = require('socket.io'); const app = require('./app'); const db = require('./db'); const jwt = require('jsonwebtoken'); require('dotenv').config(); if (!process.env.JWT_SECRET) { throw new Error('JWT_SECRET não definido no .env'); } if (!process.env.MQTT_URL) { console.warn('Warning: MQTT_URL is not defined.'); } const server = http.createServer(app); const origins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map((s) => s.trim()) : ['http://localhost:5173']; const io = new Server(server, { cors: { origin: origins, methods: ['GET', 'POST'], credentials: true, }, }); const { on } = require('./mqtt/client'); console.log('MQTT client initialized.'); // --------------------------- // Helpers de normalização // --------------------------- const toNum = (v) => { if (v === null || v === undefined || v === '') return 0; const n = typeof v === 'number' ? v : parseFloat(v); return Number.isFinite(n) ? n : 0; }; const toArr3 = (v) => { if (Array.isArray(v)) { return [toNum(v[0]), toNum(v[1]), toNum(v[2])]; } // se vier como objeto {l1,l2,l3} if (v && typeof v === 'object') { return [toNum(v.l1), toNum(v.l2), toNum(v.l3)]; } return [0, 0, 0]; }; const normalizeStatus = (rawStatus) => { const s = String(rawStatus || '').toLowerCase(); if (s.includes('charging')) return '⚡ Charging'; if (s.includes('ready')) return '🟢 Ready'; if (s.includes('fault') || s.includes('error')) return '⚠️ Fault'; if (s.includes('wait')) return '⚡ Wait'; if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.'; if (s.includes('vent')) return '💨 Vent'; // fallback: devolve string original se não bater em nada return rawStatus || '—'; }; // Normaliza eventos de status (realtime) function normalizeChargingStatus(data = {}) { const chargerId = data.charger_id || data.chargerId || data.id; const powerArr = toArr3(data.power || data.raw?.power); const voltageArr = toArr3(data.voltage || data.raw?.voltage); const currentArr = toArr3(data.current || data.raw?.current); const status = normalizeStatus(data.status || data.state || data.raw?.state); const chargingTime = toNum(data.charging_time) || toNum(data.chargingTime) || toNum(data.raw?.chargingTime) || toNum(data.raw?.sessionTime); const consumption = toNum(data.consumption) || toNum(data.raw?.consumption); const chargingCurrent = toNum(data.charging_current) || toNum(data.chargingCurrent) || currentArr[0]; return { charger_id: chargerId, mqtt_topic: data.mqtt_topic || data.mqttTopic, status, stateCode: data.stateCode || data.raw?.stateCode || undefined, consumption, charging_time: chargingTime, charging_current: chargingCurrent, power: powerArr, voltage: voltageArr, current: currentArr, raw: data.raw || data, // mantém raw p/ debug, mas já limpinho updated_at: new Date().toISOString(), }; } // Normaliza eventos de config (quando o carregador manda config) function normalizeChargingConfig(data = {}) { const chargerId = data.charger_id || data.chargerId || data.id; // se vierem chaves diferentes, mapeia const cfg = data.config || data.raw?.config || data; return { charger_id: chargerId, mqtt_topic: data.mqtt_topic || data.mqttTopic, config: { max_charging_current: cfg.max_charging_current ?? cfg.maxChargingCurrent ?? cfg.max_current ?? cfg.maxCurrent ?? undefined, require_auth: cfg.require_auth ?? cfg.requireAuth ?? undefined, rcm_enabled: cfg.rcm_enabled ?? cfg.rcmEnabled ?? undefined, temperature_limit: cfg.temperature_limit ?? cfg.temperatureThreshold ?? cfg.temp_limit ?? undefined, }, raw: data.raw || data, updated_at: new Date().toISOString(), }; } // --------------------------- // auth middleware do socket // --------------------------- io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) return next(new Error('Authentication error: token required')); try { const payload = jwt.verify(token, process.env.JWT_SECRET); socket.user = payload; next(); } catch (err) { next(new Error('Authentication error')); } }); io.on('connection', (socket) => { console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`); // join rooms apenas do user autenticado socket.on('joinChargers', async (chargerIds = []) => { try { if (!Array.isArray(chargerIds) || chargerIds.length === 0) return; const rows = await db('chargers') .whereIn('id', chargerIds) .andWhere({ user_id: socket.user.id }) .select('id'); const allowed = rows.map((r) => r.id); allowed.forEach((id) => socket.join(id)); console.log(`Socket ${socket.id} joined chargers: ${allowed}`); } catch (err) { console.error('joinChargers error:', err); } }); socket.on('charger-action', ({ chargerId, action, ampLimit }) => { console.log( `Received action "${action}" for charger ${chargerId} by user ${socket.user.id}` ); io.to(chargerId).emit('charger-action-status', 'success'); }); socket.on('disconnect', (reason) => { console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); }); }); // --------------------------- // Relay MQTT -> Socket.IO (NORMALIZADO) // --------------------------- on('charging-status', (data) => { const normalized = normalizeChargingStatus(data); const chargerId = normalized.charger_id; if (!chargerId) return; io.to(chargerId).emit('charging-status', normalized); }); on('charging-config', (data) => { const normalized = normalizeChargingConfig(data); const chargerId = normalized.charger_id; if (!chargerId) return; io.to(chargerId).emit('charging-config', normalized); }); const PORT = process.env.PORT || 4000; server.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); });