// 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;