Ajout des nouvelles modifications

This commit is contained in:
2026-03-29 14:03:19 +02:00
parent 90f10674a3
commit 98825db072
7 changed files with 514 additions and 284 deletions

View File

@@ -1,8 +1,8 @@
/**
* ============================================
* DATABASE.JS - Gestion MariaDB
* Smart Parking - BTS CIEL IR
* MODIFIÉ : ajout de getReservationById
* Smart Parking v2.0
* AJOUTÉ : expireReservations() — libère auto les places
* ============================================
*/
@@ -11,16 +11,20 @@ const bcrypt = require('bcryptjs');
require('dotenv').config();
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'smartparking_user',
password: process.env.DB_PASSWORD || 'smartparking_pass',
database: process.env.DB_NAME || 'smartparking',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'smartparking_user',
password: process.env.DB_PASSWORD || 'smartparking_pass',
database: process.env.DB_NAME || 'smartparking',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
connectionLimit: 10,
queueLimit: 0
});
// ============================================
// INITIALISATION
// ============================================
async function initDatabase() {
try {
await pool.query(`
@@ -97,9 +101,9 @@ async function initDatabase() {
)
`);
console.log('✅ Tables vérifiées/créées avec succès');
console.log('✅ Tables vérifiées/créées');
// Compte admin par défaut
// Admin par défaut
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
if (rows.length === 0) {
const hashed = await bcrypt.hash('admin123', 10);
@@ -107,27 +111,23 @@ async function initDatabase() {
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashed, 'admin']
);
console.log('✅ Administrateur par défaut créé');
console.log('✅ Admin par défaut créé');
}
// Places par défaut
// 10 places par défaut
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) {
let status = 'free';
const rand = Math.random();
if (rand > 0.85) status = 'reserved';
else if (rand > 0.60) status = 'occupied';
await pool.query(
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
[i, `SENSOR_${String(i).padStart(3, '0')}`, status]
[i, `SENSOR_${String(i).padStart(3, '0')}`, 'free']
);
}
console.log('✅ 10 places par défaut créées');
console.log('✅ 10 places créées (toutes libres)');
}
} catch (err) {
console.error("❌ Erreur lors de l'initialisation de la base :", err.message);
console.error("❌ Erreur init base :", err.message);
throw err;
}
}
@@ -166,8 +166,10 @@ async function getAllUsers() {
async function updateUser(id, updates) {
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
const values = Object.values(updates);
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]);
const [result] = await pool.query(
`UPDATE users SET ${fields} WHERE id = ?`,
[...Object.values(updates), id]
);
return { changed: result.affectedRows };
}
@@ -225,10 +227,6 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
return { id: result.insertId };
}
/**
* Récupère une réservation par son ID
* AJOUTÉ : nécessaire pour l'annulation/complétion (libération de la place)
*/
async function getReservationById(id) {
const [rows] = await pool.query(
`SELECT r.*, s.number AS spot_number
@@ -271,6 +269,53 @@ async function updateReservationStatus(id, status) {
return { changed: result.affectedRows };
}
/**
* ⭐ NOUVELLE FONCTION — Expiration automatique des réservations
*
* Cherche toutes les réservations actives dont la date+heure de fin
* est déjà dépassée, les passe en "completed" et libère les places.
*
* Appelée toutes les 60 secondes par server.js.
* Cela résout le problème des places qui restent "réservées" indéfiniment.
*/
async function expireReservations() {
// Trouver les réservations actives dont l'heure de fin est passée
const [expiredRows] = await pool.query(`
SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number
FROM reservations r
JOIN spots s ON r.spot_id = s.id
WHERE r.status = 'active'
AND TIMESTAMP(r.date, r.end_time) < NOW()
`);
for (const res of expiredRows) {
// Passer la réservation en "completed"
await pool.query(
"UPDATE reservations SET status = 'completed' WHERE id = ?",
[res.id]
);
// Libérer la place (la passer en "free")
// (le capteur Arduino prendra le relais ensuite si une voiture est encore là)
await pool.query(
"UPDATE spots SET status = 'free', last_update = NOW() WHERE id = ?",
[res.spot_id]
);
// Ajouter à l'historique
await pool.query(
'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)',
[
'Expiration réservation',
`Réservation #${res.id} expirée — place ${res.spot_number} libérée automatiquement`,
res.user_id
]
);
}
return expiredRows.length;
}
// ============================================
// HISTORIQUE
// ============================================
@@ -319,7 +364,7 @@ async function getStats(days = 7) {
}
// ============================================
// MQTT
// MQTT EVENTS
// ============================================
async function recordMqttEvent(topic, message) {
@@ -336,11 +381,11 @@ async function recordMqttEvent(topic, message) {
async function closeDatabase() {
await pool.end();
console.log('🔌 Connexions à la base fermées');
console.log('🔌 Connexions base fermées');
}
module.exports = {
pool, // exposé pour les requêtes directes si besoin
pool,
initDatabase,
closeDatabase,
// Utilisateurs
@@ -350,6 +395,7 @@ module.exports = {
// Réservations
createReservation, getReservationById, getReservationsByUser,
getAllReservations, updateReservationStatus,
expireReservations, // ← NOUVEAU
// Historique
addHistory, getHistory,
// Stats

View File

@@ -1,12 +1,11 @@
{
"name": "smart-parking-server",
"version": "1.0.0",
"description": "Backend Smart Parking avec MariaDB et Docker",
"version": "2.0.0",
"description": "Backend Smart Parking avec MariaDB, Docker et MQTT Arduino",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"init-db": "node db/init.js"
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
@@ -16,7 +15,8 @@
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"mqtt": "^5.3.4"
},
"devDependencies": {
"nodemon": "^3.0.1"

View File

@@ -1,70 +1,200 @@
const express = require('express');
const cors = require('cors');
/**
* ============================================
* SERVER.JS - Serveur principal Smart Parking
* VERSION 2.0 - MQTT Arduino + Expiration réservations
* ============================================
*/
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const path = require('path');
const mqtt = require('mqtt');
require('dotenv').config();
const db = require('./db/database');
const db = require('./db/database');
const apiRoutes = require('./routes/api');
const PORT = process.env.PORT || 3000;
const app = express();
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, '..')));
app.use('/api', apiRoutes);
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'index.html'));
});
app.get('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')));
app.get('/dashboard', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html'));
});
// ============================================
// CONNEXION MQTT (Mosquitto sur le Raspberry Pi)
// ============================================
async function startServer() {
try {
await db.initDatabase();
app.listen(PORT, () => {
console.log(`
╔══════════════════════════════════════════════════╗
║ 🅿️ SMART PARKING SERVER - PRÊT POUR DOCKER ║
╠══════════════════════════════════════════════════╣
║ 🌐 Port : ${PORT}
║ 🗄️ Base : MariaDB (${process.env.DB_HOST})
║ 🔐 JWT sécurisé
╚══════════════════════════════════════════════════╝
`);
/**
* Topics attendus depuis l'Arduino :
*
* smartparking/sensor/1 → "1" = voiture détectée (occupée)
* smartparking/sensor/1 → "0" = place libre
*
* L'Arduino publie sur ce topic via le shield Ethernet/WiFi.
* Le numéro à la fin correspond au numéro de place (1 à N).
*
* Pour tester sans Arduino (ligne de commande sur le Pi) :
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "1"
* mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "0"
*/
const MQTT_HOST = process.env.MQTT_HOST || 'localhost';
const MQTT_PORT = process.env.MQTT_PORT || 1883;
const MQTT_TOPIC = 'smartparking/sensor/#'; // # = wildcard, reçoit tous les capteurs
let mqttClient = null;
function connectMQTT() {
const brokerUrl = `mqtt://${MQTT_HOST}:${MQTT_PORT}`;
console.log(`🔌 Connexion au broker MQTT : ${brokerUrl}`);
mqttClient = mqtt.connect(brokerUrl, {
clientId: 'smartparking-server-' + Math.random().toString(16).slice(3),
keepalive: 60,
reconnectPeriod: 5000, // Reconnexion automatique toutes les 5s si coupure
connectTimeout: 10000
});
setInterval(async () => {
try {
const spots = await db.getAllSpots();
const total = spots.length;
const free = spots.filter(s => s.status === 'free').length;
const occupied = spots.filter(s => s.status === 'occupied').length;
const reserved = spots.filter(s => s.status === 'reserved').length;
await db.recordStats(total, free, occupied, reserved);
} catch (err) {
console.error('❌ Erreur stats:', err.message);
}
}, 5 * 60 * 1000);
mqttClient.on('connect', () => {
console.log('✅ MQTT connecté au broker Mosquitto');
mqttClient.subscribe(MQTT_TOPIC, (err) => {
if (err) {
console.error('❌ Erreur abonnement MQTT :', err.message);
} else {
console.log(`📡 Abonné au topic : ${MQTT_TOPIC}`);
}
});
});
} catch (err) {
console.error('❌ Erreur au démarrage :', err);
process.exit(1);
}
// Message reçu depuis un capteur Arduino
mqttClient.on('message', async (topic, messageBuffer) => {
const message = messageBuffer.toString().trim();
const topicParts = topic.split('/'); // ['smartparking', 'sensor', '1']
const spotNumber = parseInt(topicParts[2]); // numéro de place
if (isNaN(spotNumber)) {
console.warn(`⚠️ Topic MQTT invalide : ${topic}`);
return;
}
console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`);
// "1" = voiture présente → occupée | "0" = libre
const newStatus = message === '1' ? 'occupied' : 'free';
try {
// Récupérer la place par son numéro
const spots = await db.getAllSpots();
const spot = spots.find(s => s.number === spotNumber);
if (!spot) {
console.warn(`⚠️ Place numéro ${spotNumber} introuvable en base`);
return;
}
// Ne pas écraser une place RÉSERVÉE avec un simple signal capteur
// (la réservation prime sur le capteur physique)
if (spot.status === 'reserved' && newStatus === 'occupied') {
console.log(` Place ${spotNumber} déjà réservée — capteur ignoré`);
await db.recordMqttEvent(topic, message);
return;
}
// Mettre à jour en base
await db.updateSpotStatus(spot.id, newStatus);
await db.recordMqttEvent(topic, message);
console.log(`✅ Place ${spotNumber} mise à jour → ${newStatus}`);
} catch (err) {
console.error('❌ Erreur traitement message MQTT :', err.message);
}
});
mqttClient.on('error', (err) => {
console.error('❌ Erreur MQTT :', err.message);
});
mqttClient.on('reconnect', () => {
console.log('🔄 Reconnexion MQTT en cours...');
});
mqttClient.on('offline', () => {
console.warn('⚠️ Client MQTT hors-ligne');
});
}
// ============================================
// DÉMARRAGE DU SERVEUR
// ============================================
async function startServer() {
try {
// 1. Initialiser la base de données
await db.initDatabase();
// 2. Démarrer le serveur HTTP
app.listen(PORT, () => {
console.log(`
╔══════════════════════════════════════════════════════╗
║ 🅿️ SMART PARKING SERVER v2.0 - PRÊT ║
╠══════════════════════════════════════════════════════╣
║ 🌐 Port HTTP : ${PORT}
║ 🗄️ Base : MariaDB (${process.env.DB_HOST || 'localhost'})
║ 📡 MQTT : ${MQTT_HOST}:${MQTT_PORT}
║ 🔐 JWT sécurisé
╚══════════════════════════════════════════════════════╝
`);
});
// 3. Connecter le client MQTT (broker Mosquitto)
connectMQTT();
// 4. Enregistrer les statistiques toutes les 5 minutes
setInterval(async () => {
try {
const spots = await db.getAllSpots();
const total = spots.length;
const free = spots.filter(s => s.status === 'free').length;
const occupied = spots.filter(s => s.status === 'occupied').length;
const reserved = spots.filter(s => s.status === 'reserved').length;
await db.recordStats(total, free, occupied, reserved);
} catch (err) {
console.error('❌ Erreur stats :', err.message);
}
}, 5 * 60 * 1000);
// 5. ⭐ EXPIRATION AUTOMATIQUE DES RÉSERVATIONS toutes les minutes
// Libère les places dont l'heure de fin est dépassée
setInterval(async () => {
try {
const count = await db.expireReservations();
if (count > 0) {
console.log(`${count} réservation(s) expirée(s) — places libérées`);
}
} catch (err) {
console.error('❌ Erreur expiration réservations :', err.message);
}
}, 60 * 1000); // toutes les 60 secondes
} catch (err) {
console.error('❌ Erreur au démarrage :', err);
process.exit(1);
}
}
// Arrêt propre
process.on('SIGINT', async () => {
console.log('\n🛑 Arrêt du serveur...');
await db.closeDatabase();
process.exit(0);
console.log('\n🛑 Arrêt du serveur...');
if (mqttClient) mqttClient.end();
await db.closeDatabase();
process.exit(0);
});
startServer();