Initial commit
This commit is contained in:
575
routes/chargers.js
Executable file
575
routes/chargers.js
Executable file
@@ -0,0 +1,575 @@
|
||||
// routes/chargers.js
|
||||
const express = require('express');
|
||||
const { body, param, validationResult } = require('express-validator');
|
||||
const router = express.Router();
|
||||
const verifyToken = require('../middleware/verifyToken');
|
||||
const db = require('../db');
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const mqttClient = require('../mqtt/client');
|
||||
|
||||
router.use(verifyToken);
|
||||
|
||||
function handleValidation(req, res, next) {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, errors: errors.array() });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// throttling simples em memória (por charger)
|
||||
const lastConfigUpdateAt = new Map();
|
||||
|
||||
// GET /api/chargers
|
||||
router.get('/', async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
try {
|
||||
const chargers = await db('chargers')
|
||||
.where({ user_id: userId })
|
||||
.select('*');
|
||||
res.json({ success: true, data: chargers });
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar carregadores:', err);
|
||||
res.status(500).json({ success: false, message: 'Erro no servidor' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/chargers/:id
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').isUUID()],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: 'Carregador não encontrado' });
|
||||
}
|
||||
|
||||
let config = null;
|
||||
try {
|
||||
config = await db('charger_configs')
|
||||
.where({ charger_id: charger.id })
|
||||
.first();
|
||||
} catch (cfgErr) {
|
||||
console.error('[GET charger] erro ao buscar charger_configs:', cfgErr);
|
||||
}
|
||||
|
||||
if (!config)
|
||||
config = {
|
||||
charger_id: charger.id,
|
||||
max_charging_current: 32,
|
||||
require_auth: false,
|
||||
rcm_enabled: false,
|
||||
temperature_limit: 60,
|
||||
};
|
||||
|
||||
const numericFields = [
|
||||
'power_l1',
|
||||
'power_l2',
|
||||
'power_l3',
|
||||
'voltage_l1',
|
||||
'voltage_l2',
|
||||
'voltage_l3',
|
||||
'current_l1',
|
||||
'current_l2',
|
||||
'current_l3',
|
||||
'charging_current',
|
||||
'consumption',
|
||||
];
|
||||
|
||||
numericFields.forEach((field) => {
|
||||
const v = charger[field];
|
||||
charger[field] =
|
||||
v === null || v === undefined || v === '' ? 0 : Number(v);
|
||||
if (Number.isNaN(charger[field])) charger[field] = 0;
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: { ...charger, config } });
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar carregador:', err);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? err.message || 'Erro no servidor'
|
||||
: 'Erro no servidor',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/chargers
|
||||
router.post(
|
||||
'/',
|
||||
[
|
||||
body('location')
|
||||
.exists()
|
||||
.withMessage('O campo location é obrigatório')
|
||||
.isString()
|
||||
.withMessage('Location deve ser uma string')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('Location não pode estar vazio')
|
||||
.trim(),
|
||||
],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { location } = req.body;
|
||||
const userId = req.user.id;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let chargerID;
|
||||
do {
|
||||
chargerID = crypto.randomBytes(6).toString('hex');
|
||||
} while (
|
||||
await db('chargers').where({ mqtt_topic: chargerID }).first()
|
||||
);
|
||||
|
||||
const mqtt_topic = chargerID;
|
||||
const mqtt_user = chargerID;
|
||||
const mqtt_pass = crypto.randomBytes(6).toString('hex');
|
||||
|
||||
try {
|
||||
const [charger] = await db('chargers')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
location,
|
||||
status: 'offline',
|
||||
charging_current: 0,
|
||||
charging_time: 0,
|
||||
consumption: 0,
|
||||
power_l1: 0.0,
|
||||
power_l2: 0.0,
|
||||
power_l3: 0.0,
|
||||
voltage_l1: 0.0,
|
||||
voltage_l2: 0.0,
|
||||
voltage_l3: 0.0,
|
||||
current_l1: 0.0,
|
||||
current_l2: 0.0,
|
||||
current_l3: 0.0,
|
||||
mqtt_user,
|
||||
mqtt_pass,
|
||||
mqtt_topic,
|
||||
updated_at: now,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
await db('charger_configs').insert({
|
||||
charger_id: charger.id,
|
||||
max_charging_current: 32,
|
||||
require_auth: false,
|
||||
rcm_enabled: false,
|
||||
temperature_limit: 60,
|
||||
config_received_at: now,
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
'http://localhost:7000/client/create',
|
||||
{
|
||||
client_name: charger.mqtt_user,
|
||||
chargeID: charger.mqtt_topic,
|
||||
password: charger.mqtt_pass,
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Erro ao criar cliente Mosquitto:',
|
||||
err?.response?.data || err.message
|
||||
);
|
||||
}
|
||||
|
||||
res.status(201).json({ success: true, data: charger });
|
||||
} catch (err) {
|
||||
console.error('Erro ao criar carregador:', err);
|
||||
res.status(500).json({ success: false, message: 'Erro no servidor' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PUT /api/chargers/:id
|
||||
router.put(
|
||||
'/:id',
|
||||
[
|
||||
param('id').isUUID(),
|
||||
body('charger').optional().isObject(),
|
||||
body('config').optional().isObject(),
|
||||
body('location').optional().isString(), // compat front
|
||||
],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
let { charger = {}, config = {} } = req.body;
|
||||
|
||||
// ✅ compatibilidade com front antigo: { location }
|
||||
if (req.body.location && !charger.location) {
|
||||
charger.location = req.body.location;
|
||||
}
|
||||
|
||||
const trx = await db.transaction();
|
||||
|
||||
const ALLOWED_CHARGER_FIELDS = ['location'];
|
||||
const safeChargerUpdate = Object.fromEntries(
|
||||
Object.entries(charger).filter(([k]) =>
|
||||
ALLOWED_CHARGER_FIELDS.includes(k)
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
let updatedCharger;
|
||||
|
||||
if (Object.keys(safeChargerUpdate).length > 0) {
|
||||
[updatedCharger] = await trx('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.update({
|
||||
...safeChargerUpdate,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.returning('*');
|
||||
} else {
|
||||
updatedCharger = await trx('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
}
|
||||
|
||||
if (!updatedCharger) {
|
||||
await trx.rollback();
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: 'Carregador não encontrado' });
|
||||
}
|
||||
|
||||
if (Object.keys(config).length > 0) {
|
||||
const existing = await trx('charger_configs')
|
||||
.where({ charger_id: id })
|
||||
.first();
|
||||
|
||||
const data = {
|
||||
...config,
|
||||
config_received_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await trx('charger_configs')
|
||||
.where({ charger_id: id })
|
||||
.update(data);
|
||||
} else {
|
||||
await trx('charger_configs')
|
||||
.insert({ charger_id: id, ...data });
|
||||
}
|
||||
}
|
||||
|
||||
await trx.commit();
|
||||
res.json({ success: true, data: updatedCharger });
|
||||
} catch (err) {
|
||||
await trx.rollback();
|
||||
console.error('Erro ao atualizar carregador:', err);
|
||||
res.status(500).json({ success: false, message: 'Erro no servidor' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE /api/chargers/:id
|
||||
router.delete(
|
||||
'/:id',
|
||||
[param('id').isUUID()],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: 'Carregador não encontrado' });
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
'http://localhost:7000/client/delete',
|
||||
{
|
||||
client_name: charger.mqtt_user,
|
||||
chargeID: charger.mqtt_topic,
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Erro ao deletar cliente Mosquitto:',
|
||||
err?.response?.data || err.message
|
||||
);
|
||||
}
|
||||
|
||||
await db('chargers').where({ id }).del();
|
||||
res.json({ success: true, message: 'Carregador excluído com sucesso' });
|
||||
} catch (err) {
|
||||
console.error('Erro ao excluir carregador:', err);
|
||||
res.status(500).json({ success: false, message: 'Erro no servidor' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PUT /api/chargers/:id/config
|
||||
router.put(
|
||||
'/:id/config',
|
||||
[param('id').isUUID(), body('config').isObject()],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { config } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Charger not found or unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
// lê config atual
|
||||
const existing = await db('charger_configs')
|
||||
.where({ charger_id: id })
|
||||
.first();
|
||||
|
||||
// throttle simples por charger
|
||||
const nowMs = Date.now();
|
||||
const lastMs = lastConfigUpdateAt.get(id) || 0;
|
||||
const tooSoon = nowMs - lastMs < 800;
|
||||
|
||||
// se veio max_charging_current, normaliza/clampa
|
||||
let safeConfig = { ...config };
|
||||
if (safeConfig.max_charging_current !== undefined) {
|
||||
const safeAmp = Math.max(6, Math.min(Number(safeConfig.max_charging_current), 64));
|
||||
safeConfig.max_charging_current = safeAmp;
|
||||
}
|
||||
|
||||
const onlyAmp =
|
||||
Object.keys(safeConfig).length === 1 &&
|
||||
safeConfig.max_charging_current !== undefined;
|
||||
|
||||
// dedupe: mesmo valor -> responde ok sem update nem mqtt
|
||||
if (
|
||||
existing &&
|
||||
onlyAmp &&
|
||||
existing.max_charging_current === safeConfig.max_charging_current
|
||||
) {
|
||||
return res.json({ success: true, data: existing, message: 'Config unchanged' });
|
||||
}
|
||||
|
||||
// se é update muito rápido e igual ao que já está, ignora
|
||||
if (tooSoon && existing && onlyAmp) {
|
||||
return res.json({ success: true, data: existing, message: 'Throttled' });
|
||||
}
|
||||
|
||||
// ✅ upsert
|
||||
let updatedConfig;
|
||||
if (existing) {
|
||||
[updatedConfig] = await db('charger_configs')
|
||||
.where({ charger_id: id })
|
||||
.update({
|
||||
...safeConfig,
|
||||
config_received_at: new Date().toISOString(),
|
||||
})
|
||||
.returning('*');
|
||||
} else {
|
||||
[updatedConfig] = await db('charger_configs')
|
||||
.insert({
|
||||
charger_id: id,
|
||||
...safeConfig,
|
||||
config_received_at: new Date().toISOString(),
|
||||
})
|
||||
.returning('*');
|
||||
}
|
||||
|
||||
lastConfigUpdateAt.set(id, nowMs);
|
||||
|
||||
const keyMap = {
|
||||
max_charging_current: 'maxChargingCurrent',
|
||||
require_auth: 'requireAuth',
|
||||
temperature_limit: 'temperatureThreshold',
|
||||
};
|
||||
|
||||
Object.entries(safeConfig).forEach(([dbKey, value]) => {
|
||||
const publishKey = keyMap[dbKey] || dbKey;
|
||||
mqttClient.sendConfig(charger.mqtt_topic, publishKey, value);
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: updatedConfig });
|
||||
} catch (err) {
|
||||
console.error('Error updating config:', err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ success: false, message: 'Server error' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/chargers/:id/schedule
|
||||
router.get(
|
||||
'/:id/schedule',
|
||||
[param('id').isUUID()],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: 'Carregador não encontrado' });
|
||||
}
|
||||
|
||||
try {
|
||||
const schedules = await db('charger_schedules')
|
||||
.where({ charger_id: id })
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
return res.json({ success: true, data: schedules });
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar schedules:', err);
|
||||
return res.json({ success: true, data: [] });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/chargers/:id/schedule
|
||||
router.post(
|
||||
'/:id/schedule',
|
||||
[
|
||||
param('id').isUUID(),
|
||||
body('start').matches(/^\d{2}:\d{2}$/).withMessage('start inválido'),
|
||||
body('end').matches(/^\d{2}:\d{2}$/).withMessage('end inválido'),
|
||||
body('repeat')
|
||||
.isIn(['everyday', 'weekdays', 'weekends'])
|
||||
.withMessage('repeat inválido'),
|
||||
],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { start, end, repeat } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: 'Carregador não encontrado' });
|
||||
}
|
||||
|
||||
try {
|
||||
const [inserted] = await db('charger_schedules')
|
||||
.insert({
|
||||
charger_id: id,
|
||||
start,
|
||||
end,
|
||||
repeat,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
return res.status(201).json({ success: true, data: inserted });
|
||||
} catch (err) {
|
||||
console.error('Erro ao criar schedule:', err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ success: false, message: 'Erro ao criar schedule' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/chargers/:id/action
|
||||
router.post(
|
||||
'/:id/action',
|
||||
[
|
||||
param('id').isUUID().withMessage('ID do carregador inválido'),
|
||||
body('action').isIn(['start', 'stop']).withMessage('Ação inválida'),
|
||||
body('ampLimit')
|
||||
.optional()
|
||||
.isInt({ min: 6, max: 64 })
|
||||
.withMessage('ampLimit deve estar entre 6 e 64'),
|
||||
],
|
||||
handleValidation,
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { action, ampLimit } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const charger = await db('chargers')
|
||||
.where({ id, user_id: userId })
|
||||
.first();
|
||||
|
||||
if (!charger) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Carregador não encontrado ou não autorizado',
|
||||
});
|
||||
}
|
||||
|
||||
if (ampLimit !== undefined) {
|
||||
const safeAmp = Math.max(6, Math.min(Number(ampLimit), 64));
|
||||
|
||||
await db('charger_configs')
|
||||
.where({ charger_id: id })
|
||||
.update({
|
||||
max_charging_current: safeAmp,
|
||||
config_received_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
mqttClient.sendConfig(
|
||||
charger.mqtt_topic,
|
||||
'maxChargingCurrent',
|
||||
safeAmp
|
||||
);
|
||||
}
|
||||
|
||||
const enable = action === 'start';
|
||||
mqttClient.sendEnable(charger.mqtt_topic, enable);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `Comando '${action}' enviado com sucesso`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Erro ao processar ação '${action}' para carregador ${id}:`,
|
||||
err
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ success: false, message: 'Erro ao processar ação' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user