refact
This commit is contained in:
2
.env
2
.env
@@ -1,6 +1,8 @@
|
||||
DATABASE_URL=postgres://postgres:123QWEasd@localhost:5432/evstation
|
||||
PORT=4000
|
||||
|
||||
NODE_ENV=production
|
||||
|
||||
JWT_SECRET=umaChaveMuitoForteESegura123!
|
||||
|
||||
MQTT_URL=mqtt://localhost:1883
|
||||
|
||||
49
app.js
49
app.js
@@ -1,49 +0,0 @@
|
||||
// app.js
|
||||
require('dotenv').config();
|
||||
|
||||
// Enforce presence of JWT_SECRET
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.error('Error: JWT_SECRET is not defined in environment.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const express = require('express');
|
||||
|
||||
// Route modules
|
||||
const usersRoutes = require('./routes/users');
|
||||
const chargerRoutes = require('./routes/chargers');
|
||||
const sessionsRoutes = require('./routes/charger_sessions');
|
||||
const pushRoutes = require('./routes/push'); // ✅ ADICIONA ISTO
|
||||
|
||||
const app = express();
|
||||
|
||||
// Global middlewares
|
||||
app.use(express.json());
|
||||
|
||||
// Public routes (no auth)
|
||||
app.use('/api/users', usersRoutes);
|
||||
|
||||
// Protected routes
|
||||
// ✅ NOTA: chargers/sessions/push já têm router.use(verifyToken) internamente
|
||||
// portanto não precisas passar verifyToken aqui.
|
||||
app.use('/api/chargers', chargerRoutes);
|
||||
app.use('/api/charger_sessions', sessionsRoutes);
|
||||
app.use('/api/push', pushRoutes); // ✅ ISTO resolve /api/push/vapid-public-key
|
||||
|
||||
// Health check opcional (bom para produção)
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ success: true, ok: true });
|
||||
});
|
||||
|
||||
// Error handler (catch-all)
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Unhandled error:', err);
|
||||
res
|
||||
.status(err.status || 500)
|
||||
.json({
|
||||
success: false,
|
||||
message: err.message || 'Internal Server Error',
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
15
db.js
15
db.js
@@ -1,15 +0,0 @@
|
||||
// ./db.js
|
||||
require('dotenv').config();
|
||||
const knex = require('knex');
|
||||
const knexfile = require('./knexfile');
|
||||
|
||||
// Pega o ambiente (default pra 'development')
|
||||
const environment = process.env.NODE_ENV || 'development';
|
||||
|
||||
// Recupera só as configurações daquele ambiente
|
||||
const config = knexfile[environment];
|
||||
|
||||
// Inicializa o Knex com esse config
|
||||
const db = knex(config);
|
||||
|
||||
module.exports = db;
|
||||
54
knexfile.js
54
knexfile.js
@@ -1,11 +1,55 @@
|
||||
// knexfile.js
|
||||
require('dotenv').config();
|
||||
|
||||
function must(name) {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`${name} não definido no .env`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const shared = {
|
||||
client: 'pg',
|
||||
migrations: {
|
||||
directory: './src/db/migrations',
|
||||
},
|
||||
};
|
||||
|
||||
function buildConnectionFromEnv() {
|
||||
// Se houver DATABASE_URL, usa-o.
|
||||
// Se PGSSL=true, aplica ssl no formato esperado pelo driver pg (dentro de connection).
|
||||
const ssl =
|
||||
process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined;
|
||||
|
||||
if (process.env.DATABASE_URL) {
|
||||
// knex aceita string, mas o ssl precisa estar no objeto:
|
||||
return ssl
|
||||
? { connectionString: process.env.DATABASE_URL, ssl }
|
||||
: process.env.DATABASE_URL;
|
||||
}
|
||||
|
||||
// fallback para vars soltas
|
||||
return {
|
||||
host: process.env.PGHOST || '127.0.0.1',
|
||||
port: Number(process.env.PGPORT || 5432),
|
||||
user: process.env.PGUSER || 'postgres',
|
||||
password: process.env.PGPASSWORD || 'postgres',
|
||||
database: process.env.PGDATABASE || 'evse',
|
||||
...(ssl ? { ssl } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
development: {
|
||||
client: 'pg',
|
||||
connection: process.env.DATABASE_URL,
|
||||
migrations: {
|
||||
directory: './migrations',
|
||||
},
|
||||
...shared,
|
||||
connection: buildConnectionFromEnv(),
|
||||
},
|
||||
|
||||
production: {
|
||||
...shared,
|
||||
// Em produção normalmente queres obrigar DATABASE_URL (se for o teu caso):
|
||||
// connection: must('DATABASE_URL'),
|
||||
// Mas mantendo compatível com vars soltas:
|
||||
connection: buildConnectionFromEnv(),
|
||||
pool: { min: 2, max: 10 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET não definido no .env');
|
||||
}
|
||||
|
||||
function verifyToken(req, res, next) {
|
||||
const authHeader =
|
||||
req.headers['authorization'] || req.headers['Authorization'];
|
||||
|
||||
if (!authHeader) {
|
||||
return res.status(403).json({ error: 'Token não fornecido' });
|
||||
}
|
||||
|
||||
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
||||
if (!match) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: 'Token malformado. Use "Bearer <token>"' });
|
||||
}
|
||||
|
||||
const token = match[1];
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, payload) => {
|
||||
if (err) {
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
return res.status(403).json({ error: 'Sessão expirada' });
|
||||
}
|
||||
return res.status(403).json({ error: 'Token inválido' });
|
||||
}
|
||||
|
||||
if (!payload?.id) {
|
||||
return res.status(403).json({ error: 'Token inválido' });
|
||||
}
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = verifyToken;
|
||||
59
migrate_structure.sh
Executable file
59
migrate_structure.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "==> 0) Criar branch (opcional)"
|
||||
# git checkout -b refactor/estrutura-src
|
||||
|
||||
echo "==> 1) Criar pastas da nova estrutura"
|
||||
mkdir -p src/{config,db,repositories,services,domain/normalize,mqtt/handlers,middleware,routes}
|
||||
mkdir -p src/db/migrations
|
||||
|
||||
echo "==> 2) Mover ficheiros principais"
|
||||
# app/server
|
||||
mv app.js src/app.js
|
||||
mv server.js src/server.js
|
||||
|
||||
# db
|
||||
mv db.js src/db/knex.js
|
||||
|
||||
# knexfile continua na raiz (ok)
|
||||
|
||||
echo "==> 3) Mover routes"
|
||||
mv routes/chargers.js src/routes/chargers.routes.js
|
||||
mv routes/charger_sessions.js src/routes/sessions.routes.js
|
||||
mv routes/push.js src/routes/push.routes.js
|
||||
mv routes/users.js src/routes/users.routes.js
|
||||
|
||||
echo "==> 4) Mover middleware"
|
||||
mv middleware/verifyToken.js src/middleware/auth.js
|
||||
|
||||
echo "==> 5) Mover MQTT"
|
||||
mv mqtt/client.js src/mqtt/index.js
|
||||
|
||||
echo "==> 6) Mover pushService (utils -> services)"
|
||||
mv utils/pushService.js src/services/push.service.js
|
||||
|
||||
echo "==> 7) Mover migrations"
|
||||
mv migrations/* src/db/migrations/
|
||||
|
||||
echo "==> 8) Limpar pastas antigas (se vazias)"
|
||||
rmdir routes 2>/dev/null || true
|
||||
rmdir middleware 2>/dev/null || true
|
||||
rmdir mqtt 2>/dev/null || true
|
||||
rmdir utils 2>/dev/null || true
|
||||
rmdir migrations 2>/dev/null || true
|
||||
|
||||
echo "==> 9) Criar placeholders úteis (opcional)"
|
||||
touch src/config/index.js
|
||||
touch src/services/{chargers.service.js,configs.service.js,sessions.service.js}
|
||||
touch src/repositories/{chargers.repo.js,sessions.repo.js,push.repo.js}
|
||||
touch src/domain/normalize/{chargingStatus.js,chargingConfig.js}
|
||||
touch src/middleware/{validate.js,errorHandler.js}
|
||||
touch src/mqtt/publishers.js
|
||||
touch src/mqtt/handlers/{evse.handler.js,meter.handler.js,legacy.handler.js}
|
||||
|
||||
echo "==> 10) Mostrar nova árvore (até 4 níveis)"
|
||||
tree -a -L 4 src || true
|
||||
|
||||
echo "✅ Estrutura criada e ficheiros movidos."
|
||||
echo "⚠️ Próximo passo: corrigir os imports/paths (vai quebrar até ajustar)."
|
||||
@@ -1,74 +0,0 @@
|
||||
// migrations/20250619_create_tables.js
|
||||
exports.up = async function(knex) {
|
||||
// Create 'users' table
|
||||
await knex.schema.createTable('users', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('username', 255).notNullable().unique();
|
||||
table.string('password', 255).notNullable();
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Create 'chargers' table with new fields
|
||||
await knex.schema.createTable('chargers', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.integer('user_id').unsigned().notNullable()
|
||||
.references('id').inTable('users').onDelete('CASCADE');
|
||||
table.string('location', 255).notNullable();
|
||||
table.string('status', 50).notNullable().defaultTo('offline');
|
||||
table.integer('charging_current').notNullable().defaultTo(32);
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.string('mqtt_user', 255).notNullable();
|
||||
table.string('mqtt_pass', 255).notNullable();
|
||||
table.string('mqtt_topic', 255).notNullable().unique();
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
|
||||
// Add power and current for 3 phases, voltage and other new fields
|
||||
table.integer('charging_time').notNullable().defaultTo(0); // Total charging time
|
||||
table.decimal('consumption', 8, 2).notNullable().defaultTo(0); // Consumption (kWh)
|
||||
|
||||
// Power for 3 phases (L1, L2, L3)
|
||||
table.decimal('power_l1', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('power_l2', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('power_l3', 8, 2).notNullable().defaultTo(0);
|
||||
|
||||
// Voltage for 3 phases (L1, L2, L3)
|
||||
table.decimal('voltage_l1', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('voltage_l2', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('voltage_l3', 8, 2).notNullable().defaultTo(0);
|
||||
|
||||
// Current for 3 phases (L1, L2, L3)
|
||||
table.decimal('current_l1', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('current_l2', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('current_l3', 8, 2).notNullable().defaultTo(0);
|
||||
});
|
||||
|
||||
// Create 'charger_configs' table
|
||||
await knex.schema.createTable('charger_configs', (table) => {
|
||||
table.uuid('charger_id').primary()
|
||||
.references('id').inTable('chargers').onDelete('CASCADE');
|
||||
table.integer('max_charging_current').notNullable().defaultTo(32);
|
||||
table.boolean('require_auth').notNullable().defaultTo(false);
|
||||
table.boolean('rcm_enabled').notNullable().defaultTo(false);
|
||||
table.integer('temperature_limit').notNullable().defaultTo(60);
|
||||
table.timestamp('config_received_at').notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// Create 'charger_sessions' table
|
||||
await knex.schema.createTable('charger_sessions', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.uuid('charger_id').notNullable()
|
||||
.references('id').inTable('chargers').onDelete('CASCADE');
|
||||
table.timestamp('started_at').notNullable();
|
||||
table.timestamp('ended_at');
|
||||
table.decimal('kwh', 8, 2).notNullable().defaultTo(0);
|
||||
table.decimal('cost', 10, 2);
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('charger_sessions');
|
||||
await knex.schema.dropTableIfExists('charger_configs');
|
||||
await knex.schema.dropTableIfExists('chargers');
|
||||
await knex.schema.dropTableIfExists('users');
|
||||
};
|
||||
436
mqtt/client.js
436
mqtt/client.js
@@ -1,436 +0,0 @@
|
||||
// mqtt/client.js
|
||||
const mqtt = require('mqtt');
|
||||
const EventEmitter = require('events');
|
||||
const db = require('../db');
|
||||
const { sendPushToUser } = require('../utils/pushService');
|
||||
require('dotenv').config();
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
const MQTT_URL = process.env.MQTT_URL || 'mqtt://localhost:1883';
|
||||
const mqttUser = process.env.MQTT_USER || 'admin';
|
||||
const mqttPass = process.env.MQTT_PASS || '123QWEasd';
|
||||
|
||||
const client = mqtt.connect(MQTT_URL, {
|
||||
username: mqttUser,
|
||||
password: mqttPass,
|
||||
reconnectPeriod: 2000,
|
||||
});
|
||||
|
||||
// --------------------
|
||||
// Helpers
|
||||
// --------------------
|
||||
const lastEnabled = {}; // por chargerId
|
||||
|
||||
function getStatusFromStateCode(code) {
|
||||
const map = {
|
||||
A1: '🔌 Not Conn.',
|
||||
B1: '🟡 Unauth.',
|
||||
B2: '🟢 Ready',
|
||||
C1: '⚡ Wait',
|
||||
C2: '⚡ Charging',
|
||||
D1: '💨 Vent (req)',
|
||||
D2: '💨 Vent',
|
||||
E: '❌ CP Error',
|
||||
F: '⚠️ Fault',
|
||||
};
|
||||
return map[code] || '❓ Unknown';
|
||||
}
|
||||
|
||||
function getTriple(arr) {
|
||||
return Array.isArray(arr)
|
||||
? arr.slice(0, 3).map((n) => Math.round((Number(n) || 0) * 10) / 10)
|
||||
: [0, 0, 0];
|
||||
}
|
||||
|
||||
function toOneDecimal(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return 0;
|
||||
return Math.round(n * 10) / 10;
|
||||
}
|
||||
|
||||
function shallowEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
const ak = Object.keys(a);
|
||||
const bk = Object.keys(b);
|
||||
if (ak.length !== bk.length) return false;
|
||||
for (const k of ak) {
|
||||
const av = a[k];
|
||||
const bv = b[k];
|
||||
if (Array.isArray(av) || Array.isArray(bv)) {
|
||||
if (!Array.isArray(av) || !Array.isArray(bv)) return false;
|
||||
if (av.length !== bv.length) return false;
|
||||
for (let i = 0; i < av.length; i++) {
|
||||
if (av[i] !== bv[i]) return false;
|
||||
}
|
||||
} else if (av !== bv) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Caches
|
||||
// --------------------
|
||||
|
||||
// último estado normalizado por chargerId
|
||||
const lastStateByChargerId = new Map(); // chargerId -> normalizedState
|
||||
|
||||
// cache charger por mqttTopic (evita SELECT a cada msg)
|
||||
const chargerCache = new Map(); // mqttTopic -> { charger, fetchedAt }
|
||||
const CHARGER_CACHE_TTL_MS = Number(process.env.CHARGER_CACHE_TTL_MS || 30000);
|
||||
|
||||
async function getChargerByMqttTopic(mqttTopic) {
|
||||
const cached = chargerCache.get(mqttTopic);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && now - cached.fetchedAt < CHARGER_CACHE_TTL_MS) {
|
||||
return cached.charger;
|
||||
}
|
||||
|
||||
const charger = await db('chargers').where({ mqtt_topic: mqttTopic }).first();
|
||||
if (charger) {
|
||||
chargerCache.set(mqttTopic, { charger, fetchedAt: now });
|
||||
}
|
||||
return charger;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Subscribe
|
||||
// --------------------
|
||||
client.on('connect', () => {
|
||||
console.log('[MQTT] Conectado ao broker:', MQTT_URL);
|
||||
|
||||
// ✅ tópicos fixos que TU pediste manter
|
||||
const fixedTopics = ['+/state', '+/response/config/evse'];
|
||||
|
||||
// opcional via env (mas não substitui os fixos)
|
||||
const envTopics = (process.env.MQTT_SUB_TOPICS || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const topicsToSub = [...new Set([...fixedTopics, ...envTopics])];
|
||||
|
||||
topicsToSub.forEach((t) => {
|
||||
client.subscribe(t, { qos: 0 }, (err, granted) => {
|
||||
if (err) {
|
||||
console.error('[MQTT] Falha ao subscrever', t, err.message);
|
||||
} else {
|
||||
console.log('[MQTT] Subscrito:', granted?.map((g) => g.topic).join(', ') || t);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --------------------
|
||||
// Messages
|
||||
// --------------------
|
||||
client.on('message', async (topic, message) => {
|
||||
// LOG para garantir que está a consumir
|
||||
console.log('[MQTT] msg recebida em:', topic);
|
||||
|
||||
const parts = topic.split('/');
|
||||
const mqttTopic = parts[0]; // ex: bf92842c365a
|
||||
const subtopic = parts.slice(1).join('/'); // ex: state ou response/config/evse
|
||||
|
||||
// --------------------
|
||||
// STATE
|
||||
// --------------------
|
||||
if (subtopic === 'state') {
|
||||
try {
|
||||
const payload = JSON.parse(message.toString());
|
||||
|
||||
const charger = await getChargerByMqttTopic(mqttTopic);
|
||||
if (!charger) {
|
||||
console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const chargerId = charger.id;
|
||||
const now = new Date();
|
||||
|
||||
const stateCode = String(payload.state || '').split(' ')[0];
|
||||
if (!stateCode) {
|
||||
console.warn(`[MQTT] Estado ausente/inválido para charger ID ${chargerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = getStatusFromStateCode(stateCode);
|
||||
|
||||
const [p1, p2, p3] = getTriple(payload.power);
|
||||
const [v1, v2, v3] = getTriple(payload.voltage);
|
||||
const [c1, c2, c3] = getTriple(payload.current);
|
||||
|
||||
const consumption = toOneDecimal(payload.consumption);
|
||||
const chargingTime = toOneDecimal(
|
||||
payload.chargingTime ?? payload.sessionTime
|
||||
);
|
||||
|
||||
const normalizedState = {
|
||||
status,
|
||||
charging_current: c1,
|
||||
consumption,
|
||||
charging_time: chargingTime,
|
||||
|
||||
power_l1: p1,
|
||||
power_l2: p2,
|
||||
power_l3: p3,
|
||||
|
||||
voltage_l1: v1,
|
||||
voltage_l2: v2,
|
||||
voltage_l3: v3,
|
||||
|
||||
current_l1: c1,
|
||||
current_l2: c2,
|
||||
current_l3: c3,
|
||||
};
|
||||
|
||||
const prevState = lastStateByChargerId.get(chargerId);
|
||||
const changed = !prevState || !shallowEqual(prevState, normalizedState);
|
||||
|
||||
if (changed) {
|
||||
await db('chargers')
|
||||
.where({ id: chargerId })
|
||||
.update({
|
||||
...normalizedState,
|
||||
updated_at: now.toISOString(),
|
||||
});
|
||||
|
||||
lastStateByChargerId.set(chargerId, normalizedState);
|
||||
console.log(`[DB] Estado atualizado para charger ID ${chargerId}`);
|
||||
} else {
|
||||
// só para debug — podes remover depois
|
||||
console.log(`[MQTT] Estado repetido, sem write (charger ${chargerId})`);
|
||||
}
|
||||
|
||||
// sessões start/stop (mantido)
|
||||
const previouslyEnabled = lastEnabled[chargerId] || false;
|
||||
const currentlyEnabled = stateCode === 'C2';
|
||||
|
||||
if (!previouslyEnabled && currentlyEnabled) {
|
||||
const activeSession = await db('charger_sessions')
|
||||
.where({ charger_id: chargerId })
|
||||
.whereNull('ended_at')
|
||||
.first();
|
||||
|
||||
if (!activeSession) {
|
||||
await db('charger_sessions').insert({
|
||||
charger_id: chargerId,
|
||||
started_at: now,
|
||||
kwh: consumption,
|
||||
});
|
||||
console.log(`[DB] Sessão iniciada para charger ID ${chargerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (previouslyEnabled && !currentlyEnabled) {
|
||||
const session = await db('charger_sessions')
|
||||
.where({ charger_id: chargerId })
|
||||
.whereNull('ended_at')
|
||||
.first();
|
||||
|
||||
if (session) {
|
||||
await db('charger_sessions')
|
||||
.where({ id: session.id })
|
||||
.update({ ended_at: now, kwh: consumption });
|
||||
|
||||
console.log(`[DB] Sessão finalizada para charger ID ${chargerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
lastEnabled[chargerId] = currentlyEnabled;
|
||||
|
||||
// emit para socket
|
||||
emitter.emit('charging-status', {
|
||||
charger_id: chargerId,
|
||||
mqtt_topic: mqttTopic,
|
||||
status,
|
||||
stateCode,
|
||||
consumption,
|
||||
chargingTime,
|
||||
power: [p1, p2, p3],
|
||||
voltage: [v1, v2, v3],
|
||||
current: [c1, c2, c3],
|
||||
raw: payload,
|
||||
});
|
||||
|
||||
// push (mantido)
|
||||
if (status === '⚠️ Fault' || status === '❌ CP Error') {
|
||||
await sendPushToUser(charger.user_id, {
|
||||
title: '⚠️ Erro no carregador',
|
||||
body: `${charger.location || 'Carregador'} entrou em falha.`,
|
||||
url: `/charger/${charger.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (previouslyEnabled && !currentlyEnabled) {
|
||||
await sendPushToUser(charger.user_id, {
|
||||
title: '✅ Carregamento concluído',
|
||||
body: `${charger.location || 'Carregador'} terminou o carregamento.`,
|
||||
url: `/history`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[MQTT] Erro ao processar state '${mqttTopic}':`, err);
|
||||
console.error('Payload recebido:', message.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// CONFIG RESPONSE
|
||||
// --------------------
|
||||
else if (subtopic === 'response/config/evse') {
|
||||
try {
|
||||
const payload = JSON.parse(message.toString());
|
||||
|
||||
const charger = await getChargerByMqttTopic(mqttTopic);
|
||||
if (!charger) {
|
||||
console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const configData = {
|
||||
charger_id: charger.id,
|
||||
max_charging_current: payload.maxChargingCurrent || 32,
|
||||
require_auth: !!payload.requireAuth,
|
||||
rcm_enabled: !!payload.rcm,
|
||||
temperature_limit: payload.temperatureThreshold || 60,
|
||||
config_received_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const existingConfig = await db('charger_configs')
|
||||
.where({ charger_id: charger.id })
|
||||
.first();
|
||||
|
||||
const prevCfgNorm = existingConfig
|
||||
? {
|
||||
max_charging_current: Number(existingConfig.max_charging_current) || 32,
|
||||
require_auth: !!existingConfig.require_auth,
|
||||
rcm_enabled: !!existingConfig.rcm_enabled,
|
||||
temperature_limit: Number(existingConfig.temperature_limit) || 60,
|
||||
}
|
||||
: null;
|
||||
|
||||
const nextCfgNorm = {
|
||||
max_charging_current: Number(configData.max_charging_current) || 32,
|
||||
require_auth: !!configData.require_auth,
|
||||
rcm_enabled: !!configData.rcm_enabled,
|
||||
temperature_limit: Number(configData.temperature_limit) || 60,
|
||||
};
|
||||
|
||||
const cfgChanged = !prevCfgNorm || !shallowEqual(prevCfgNorm, nextCfgNorm);
|
||||
|
||||
if (cfgChanged) {
|
||||
if (existingConfig) {
|
||||
await db('charger_configs')
|
||||
.where({ charger_id: charger.id })
|
||||
.update(configData);
|
||||
console.log(`[DB] Configuração atualizada para charger ID ${charger.id}`);
|
||||
} else {
|
||||
await db('charger_configs').insert(configData);
|
||||
console.log(`[DB] Nova configuração inserida para charger ID ${charger.id}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[MQTT] Config repetida, sem write (charger ${charger.id})`);
|
||||
}
|
||||
|
||||
emitter.emit('charging-config', {
|
||||
...configData,
|
||||
mqtt_topic: mqttTopic,
|
||||
raw: payload,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[MQTT] Erro ao processar config '${mqttTopic}':`, err);
|
||||
console.error('Payload recebido:', message.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------
|
||||
// Broker offline / checker (igual ao teu)
|
||||
// --------------------
|
||||
client.on('offline', async () => {
|
||||
console.warn('[MQTT] Broker offline');
|
||||
|
||||
try {
|
||||
const chargers = await db('chargers').select('id', 'user_id', 'location');
|
||||
const uniqueUsers = [...new Set(chargers.map((c) => c.user_id))];
|
||||
|
||||
await Promise.allSettled(
|
||||
uniqueUsers.map((userId) =>
|
||||
sendPushToUser(userId, {
|
||||
title: '📡 Broker MQTT offline',
|
||||
body: 'O sistema perdeu ligação ao broker. Alguns estados podem estar desatualizados.',
|
||||
url: '/',
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[MQTT] erro offline push:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const timeoutMinutes = Number(process.env.CHARGER_OFFLINE_MINUTES || 5);
|
||||
const limitDate = new Date(Date.now() - timeoutMinutes * 60 * 1000);
|
||||
|
||||
const offlineChargers = await db('chargers')
|
||||
.where('updated_at', '<', limitDate.toISOString())
|
||||
.andWhereNot({ status: 'offline' })
|
||||
.select('*');
|
||||
|
||||
for (const ch of offlineChargers) {
|
||||
await db('chargers')
|
||||
.where({ id: ch.id })
|
||||
.update({ status: 'offline' });
|
||||
|
||||
lastStateByChargerId.delete(ch.id);
|
||||
lastEnabled[ch.id] = false;
|
||||
|
||||
await sendPushToUser(ch.user_id, {
|
||||
title: '🔌 Carregador offline',
|
||||
body: `${ch.location || 'Carregador'} está offline há mais de ${timeoutMinutes} min.`,
|
||||
url: `/charger/${ch.id}`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[MQTT] offline checker erro:', err.message);
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
// --------------------
|
||||
// API pública
|
||||
// --------------------
|
||||
function on(event, handler) {
|
||||
emitter.on(event, handler);
|
||||
}
|
||||
|
||||
function sendConfig(chargerTopic, property, value) {
|
||||
const payload = { [property]: value };
|
||||
client.publish(`${chargerTopic}/set/config/evse`, JSON.stringify(payload), {
|
||||
qos: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function sendEnable(chargerTopic, enable) {
|
||||
client.publish(
|
||||
`${chargerTopic}/enable`,
|
||||
JSON.stringify({ enable: !!enable }),
|
||||
{ qos: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
function requestConfig(chargerTopic) {
|
||||
client.publish(`${chargerTopic}/request/config/evse`, null, { qos: 1 });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
on,
|
||||
sendConfig,
|
||||
sendEnable,
|
||||
requestConfig,
|
||||
};
|
||||
0
node_modules/@socket.io/component-emitter/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/Readme.md
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/Readme.md
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@socket.io/component-emitter/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/README.md
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/README.md
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/package.json
generated
vendored
Normal file → Executable file
0
node_modules/@types/cors/package.json
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/README.md
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/README.md
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.d.ts.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.d.ts.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.js
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.js
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.js.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/helpers.js.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.d.ts.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.d.ts.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.js
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.js
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.js.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/dist/index.js.map
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/package.json
generated
vendored
Normal file → Executable file
0
node_modules/agent-base/package.json
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/.eslintrc.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/.eslintrc.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/README.md
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/README.md
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/api.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/api.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/buffer.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/buffer.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/node.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/node.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/reporter.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/base/reporter.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/constants/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/constants/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/constants/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/constants/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/pem.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/decoders/pem.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/der.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/pem.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/lib/asn1/encoders/pem.js
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/package.json
generated
vendored
Normal file → Executable file
0
node_modules/asn1.js/package.json
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/README.md
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/README.md
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/bench.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/bench.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/index.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/abort.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/abort.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/async.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/async.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/defer.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/defer.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/iterate.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/iterate.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_asynckit.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_asynckit.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_parallel.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_parallel.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_serial.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_serial.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_serial_ordered.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/readable_serial_ordered.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/state.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/state.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/streamify.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/streamify.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/terminator.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/lib/terminator.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/package.json
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/package.json
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/parallel.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/parallel.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/serial.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/serial.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/serialOrdered.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/serialOrdered.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/stream.js
generated
vendored
Normal file → Executable file
0
node_modules/asynckit/stream.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/CHANGELOG.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/CHANGELOG.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/axios/LICENSE
generated
vendored
Normal file → Executable file
0
node_modules/axios/MIGRATION_GUIDE.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/MIGRATION_GUIDE.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/README.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/README.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.min.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.min.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.min.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/axios.min.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/browser/axios.cjs
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/browser/axios.cjs
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/browser/axios.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/browser/axios.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.min.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.min.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.min.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/esm/axios.min.js.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/node/axios.cjs
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/node/axios.cjs
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/node/axios.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/dist/node/axios.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.d.cts
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.d.cts
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/index.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/README.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/README.md
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/adapters.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/adapters.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/fetch.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/fetch.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/xhr.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/adapters/xhr.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/CancelToken.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/CancelToken.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/CanceledError.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/CanceledError.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/isCancel.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/cancel/isCancel.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/Axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/Axios.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/AxiosError.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/AxiosError.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/AxiosHeaders.js
generated
vendored
Normal file → Executable file
0
node_modules/axios/lib/core/AxiosHeaders.js
generated
vendored
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user