Initial commit

This commit is contained in:
2025-12-07 14:32:46 +00:00
commit 0a0969b8af
4726 changed files with 536089 additions and 0 deletions

225
server.js Executable file
View File

@@ -0,0 +1,225 @@
// 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}`);
});