265 lines
6.8 KiB
C
265 lines
6.8 KiB
C
|
||
|
||
// === Início de: ./package.json ===
|
||
{
|
||
"name": "evse-backend",
|
||
"version": "1.0.0",
|
||
"main": "index.js",
|
||
"scripts": {
|
||
"test": "echo \"Error: no test specified\" && exit 1"
|
||
},
|
||
"keywords": [],
|
||
"author": "",
|
||
"license": "ISC",
|
||
"description": "",
|
||
"dependencies": {
|
||
"axios": "^1.13.2",
|
||
"bcryptjs": "^3.0.2",
|
||
"dotenv": "^16.5.0",
|
||
"express": "^5.1.0",
|
||
"express-rate-limit": "^8.2.1",
|
||
"express-validator": "^7.2.1",
|
||
"jsonwebtoken": "^9.0.2",
|
||
"knex": "^3.1.0",
|
||
"mqtt": "^5.13.1",
|
||
"pg": "^8.16.0",
|
||
"socket.io": "^4.8.1",
|
||
"web-push": "^3.6.7"
|
||
},
|
||
"devDependencies": {
|
||
"nodemon": "^3.1.10"
|
||
}
|
||
}
|
||
|
||
// === Fim de: ./package.json ===
|
||
|
||
|
||
// === Início de: ./server.js ===
|
||
// 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}`);
|
||
});
|
||
|
||
// === Fim de: ./server.js ===
|