226 lines
6.0 KiB
JavaScript
Executable File
226 lines
6.0 KiB
JavaScript
Executable File
// 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}`);
|
|
});
|