Système de domotique professionnel avec architecture distribuée ESP32 + Serveur Backend API + Dashboard Next.js
Architecture • Installation • API • Sécurité • Tests
# Télécharger la dernière release
curl -L https://github.com/MyHouse-OS/MyHouseOS/releases/latest/download/docker-compose.yml -o docker-compose.yml
# Lancer les services
docker-compose up -d📦 Les images Docker sont automatiquement récupérées depuis GitHub Container Registry
|
Orchestration & Releases Script de déploiement unifié |
⚡ APIBackend Bun + Elysia REST API • WebSocket • PostgreSQL |
🖥️ DashboardFrontend Next.js Interface temps réel responsive |
|
Access Point & Router Point d'accès WiFi • Auth 2FA matérielle |
🌡️ ESP EmitterCapteur & Contrôle Température • Lumière • Chauffage • Porte |
Afficheur & LED Dashboard physique • Indicateurs LED |
- Vue d'ensemble
- Architecture
- Stack Technique
- Installation & Déploiement
- Configuration Réseau
- Authentification & Sécurité
- Base de Données
- API Reference
- Règles d'Automatisation
- WebSocket & Temps Réel
- Dashboard Web
- Tests & Qualité
- Structure du Projet
- Développement
MyHouse OS est un système de domotique ultra-sécurisé et entièrement automatisé, construit autour d'une architecture distribuée robuste. Le projet combine des microcontrôleurs ESP32, un backend haute performance basé sur Bun, et un dashboard web responsive développé avec Next.js.
✨ Architecture Distribuée - 3 ESP32 (1 serveur + 2 clients) avec répartition intelligente des rôles
🔐 Sécurité Renforcée - Authentification à deux facteurs matérielle avec validation manuelle
⚡ Temps Réel - WebSocket bidirectionnel pour synchronisation instantanée
🤖 Automatisation Intelligente - Moteur de règles événementielles avec 4+ scénarios prédéfinis
📊 Historique Complet - Enregistrement de tous les événements pour analyse et dashboards
🧪 Testabilité Professionnelle - Suite de tests complète avec couverture de code
📖 Documentation Swagger - API REST auto-documentée et interactive
Le système utilise un réseau Wi-Fi isolé avec allocation IP statique garantissant la sécurité et la stabilité.
┌─────────────────────────────────────────────────────────────────┐
│ ESP32 SERVEUR (AP Mode) │
│ IP: 192.168.4.1 │
│ │
│ • Point d'accès WiFi principal │
│ • Gestion des requêtes /link │
│ • Validation manuelle 2FA (boutons Accept/Reject) │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ BUN SERVER │ │ NEXT.JS │ │ ESP CLIENTS │
│ 192.168.4.2 │ │ 192.168.4.3 │ │ 192.168.4.4/5 │
│ :3000 │ │ :8080 │ │ │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ • API REST │ │ • Dashboard │ │ • Client #1: │
│ • WebSocket │◄──┤ • Real-time │ │ Capteur │
│ • DB │ │ updates │ │ Température │
│ • Rules │ │ • Responsive │ │ │
│ • History │ │ │ │ • Client #2: │
│ │ │ │ │ Affichage │
│ │ │ │ │ + LED │
└───────────────┘ └───────────────┘ └───────────────┘
│ ▲
│ │
└──────────────────────────────────────┘
Authentification & Contrôle
L'ordre de connexion est impératif pour le bon fonctionnement du système :
- ESP Serveur démarre en mode AP →
192.168.4.1 - PC Serveur API se connecte au WiFi ESP → reçoit
192.168.4.2 - Lancement Docker (
docker-compose up) - PC Serveur Next.js se connecte → reçoit
192.168.4.3 - ESP Clients se connectent → reçoivent
192.168.4.4et192.168.4.5
| Composant | Rôle | IP | Description |
|---|---|---|---|
| ESP32 Serveur | Access Point + Router | 192.168.4.1 |
Gestion réseau, authentification 2FA, routage des requêtes /link |
| ESP32 Client #1 | Capteur Actif | 192.168.4.4 |
Mesure température toutes les 3s, contrôle lumière/chauffage/porte |
| ESP32 Client #2 | Afficheur Passif | 192.168.4.5 |
Lecture état système, LED de statut, rafraîchissement toutes les 5s |
| PC Docker | Backend Server | 192.168.4.2:3000 |
Bun runtime, PostgreSQL, API REST, WebSocket |
| PC Dashboard | Frontend Server | 192.168.4.3:8080 |
Next.js, interface responsive, temps réel |
{
"runtime": "Bun 1.x",
"framework": "Elysia.js 1.4.19",
"database": "PostgreSQL 16 Alpine",
"orm": "Prisma 7.1.0",
"language": "TypeScript 5.x",
"containerization": "Docker + Docker Compose"
}Dépendances Principales :
- 🚀 Elysia - Framework web ultra-rapide pour Bun
- 📖 @elysiajs/swagger - Documentation API interactive
- 🔌 WebSocket natif - Communication temps réel
- 🔒 Cryptographie native Bun - Hash de mots de passe bcrypt
- 🗄️ Prisma - ORM type-safe avec migrations automatiques
- 🐘 PostgreSQL - Base de données relationnelle
Outils de Développement :
- ✅ Biome - Linter & Formatter ultra-rapide (remplace ESLint + Prettier)
- 🧪 Bun Test - Runner de tests natif avec couverture de code
- 🔍 Knip - Détection de code mort et imports inutilisés
- 🪝 Lefthook - Git hooks pour CI/CD local
- Next.js 14+ - Framework React avec SSR/SSG
- WebSocket Client - Connexion persistante au serveur Bun
- Responsive Design - Compatible mobile/tablette/desktop
- ESP32
- Capteur Température
- LED RGB
- Boutons physiques
# 1. Cloner le dépôt
git clone <repository-url>
cd BunServer
# 2. Installer les dépendances
bun install
# 3. Configuration environnement
cp .env.example .env
# Éditer .env avec vos paramètres
# 4. Démarrer PostgreSQL
docker-compose up db -d
# 5. Initialiser la base de données
bun run prisma:migrate
bun run seed
# 6. Générer le client Prisma
bun run prisma:generate
# 7. Démarrer le serveur
bun run start# Construction et démarrage de tous les services
docker-compose up --build -d
# Vérification des logs
docker-compose logs -f app
# Exécution des seeds (première fois)
docker-compose exec app bun run seedLe serveur sera accessible sur :
- 🔗 API :
http://192.168.4.2:3000 - 📖 Swagger :
http://192.168.4.2:3000/swagger - 🔌 WebSocket :
ws://192.168.4.2:3000/ws
// Configuration Point d'Accès
const char* ssid = "MyHouseOS";
const char* password = "12345678";
IPAddress localIP(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(localIP, gateway, subnet);
WiFi.softAP(ssid, password);WiFi.begin("MyHouseOS", "SuperSecurePassword123!");
// Attendre l'attribution IP
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
// IP attribuée automatiquement : 192.168.4.4 ou .5# Database
DATABASE_URL="postgresql://root:root@db:5432/myhouse"
# Server
PORT_BUN_SERVER=3000
PORT_WEB_SERVER=8080
NODE_ENV=production
# Security (optionnel - utilise des clés par défaut)
ENCRYPTION_KEY=your-32-char-secret-key-here
ENCRYPTION_SALT=your-fixed-salt-hereLe système implémente une authentification à deux facteurs matérielle (Hardware 2FA) unique en son genre.
sequenceDiagram
participant User as Utilisateur
participant EC as ESP Client
participant ES as ESP Serveur
participant BS as Bun Server
participant DB as PostgreSQL
User->>EC: Appuie sur bouton "Link"
EC->>ES: POST /link { id: "clientID" }
ES->>User: Attente validation (LED clignote)
User->>ES: Appuie bouton "Accept" ou "Reject"
alt Accepté
ES->>BS: GET /check?id=clientID
BS->>DB: SELECT * FROM Client WHERE id=clientID
alt Client existe déjà
DB-->>BS: { exists: true, token: "abc123" }
BS-->>ES: { exists: true, token: "abc123" }
ES-->>EC: 200 OK { token: "abc123" }
else Nouveau client
DB-->>BS: { exists: false }
ES->>ES: Génère token aléatoire
ES->>BS: POST /auth { id: "clientID", token: "xyz789" }
Note over ES,BS: Headers: Authorization: master:master
BS->>DB: INSERT INTO Client VALUES (...)
DB-->>BS: Success
BS-->>ES: 200 OK
ES-->>EC: 200 OK { token: "xyz789" }
end
EC->>EC: Stocke token en EEPROM
else Rejeté
ES-->>EC: 403 Forbidden
end
Toutes les routes (sauf / et /swagger) nécessitent l'en-tête :
Authorization: clientID:tokenExemple :
curl -X POST http://192.168.4.2:3000/temp \
-H "Authorization: LivingRoomESP:a1b2c3d4e5f6" \
-H "Content-Type: application/json" \
-d '{"temp": "22.5"}'// src/middleware/auth.ts
export const authMiddleware = (app: Elysia) =>
app.derive(async ({ headers, set }) => {
const [clientId, clientToken] = headers.authorization.split(":");
// Récupération client en DB
const client = await prisma.client.findUnique({
where: { ClientID: clientId }
});
// Vérification token crypté
const decryptedToken = decrypt(client.ClientToken);
if (decryptedToken !== clientToken) {
throw new Error("Invalid credentials");
}
return { user: client };
});Tokens ESP : Chiffrement AES-256 symétrique
// src/utils/crypto.ts
import { createCipheriv, createDecipheriv } from "crypto";
export const encrypt = (text: string): string => {
const cipher = createCipheriv("aes-256-cbc", SECRET_KEY, IV);
return cipher.update(text, "utf8", "hex") + cipher.final("hex");
};Mots de passe Dashboard : Bcrypt avec salting automatique
// Utilise le hachage natif Bun (Argon2 ou bcrypt)
const passwordHash = await Bun.password.hash("root");| Type | ID | Token/Password | Table | Usage |
|---|---|---|---|---|
| Master | master |
master |
Client |
Authentification ESP Serveur → API /auth |
| Root User | root |
root |
User |
Connexion Dashboard administrateur |
| Dashboard Client | root |
root |
Client |
Authentification Dashboard → API |
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
username String @unique
password String // Hash bcrypt
}
model Client {
id Int @id @default(autoincrement())
ClientID String @unique // Identifiant ESP unique
ClientToken String // Token AES-256 chiffré
}
model HomeState {
id Int @id @default(autoincrement())
temperature String // °C en string pour précision
light Boolean @default(false)
door Boolean @default(false)
heat Boolean @default(false)
updatedAt DateTime @default(now()) @updatedAt
createdAt DateTime @default(now())
}
model History {
id Int @id @default(autoincrement())
type EventType // TEMPERATURE | LIGHT | DOOR | HEAT
value String
createdAt DateTime @default(now())
}
enum EventType {
TEMPERATURE
LIGHT
DOOR
HEAT
}# Créer une nouvelle migration
bun run prisma:migrate
# Pousser le schéma (développement rapide)
bun run prisma:push
# Visualiser la DB (interface GUI)
bun run prisma:studioAffiche la bannière ASCII MyHouse OS.
curl http://192.168.4.2:3000/Réponse :
__ __ _ _ ___ ____
| \/ |_ _| | | | ___ _ _ ___ ___ / _ \/ ___|
| |\/| | | | | |_| |/ _ \| | | / __|/ _ \ | | | \___ \
| | | | |_| | _ | (_) | |_| \__ \ __/ | |_| |___) |
|_| |_|\__, |_| |_|\___/ \__,_|___/\___| \___/|____/
|___/
Documentation API interactive Swagger UI.
http://192.168.4.2:3000/swagger
Enregistrement d'un nouveau client ESP
Headers :
Authorization: master:masterBody :
{
"id": "LivingRoomESP",
"token": "a1b2c3d4e5f6"
}Réponse :
{
"status": "OK",
"message": "Client LivingRoomESP registered successfully",
"client": "LivingRoomESP"
}Vérifier l'existence d'un client
Headers :
Authorization: master:masterQuery :
?id=LivingRoomESP
Réponse (existe) :
{
"exists": true,
"token": "a1b2c3d4e5f6"
}Réponse (n'existe pas) :
{
"exists": false
}Récupérer la température actuelle
Headers :
Authorization: LivingRoomESP:a1b2c3d4e5f6Réponse :
{
"temp": "22.5",
"status": "OK"
}Mettre à jour la température
Headers :
Authorization: LivingRoomESP:a1b2c3d4e5f6
Content-Type: application/jsonBody :
{
"temp": "23.5"
}Réponse :
{
"message": "Temperature updated",
"temp": "23.5",
"status": "OK"
}Side Effects :
- ✅ Enregistrement dans
Historytable - ✅ Émission événement
STATE_CHANGE→ déclenchement règles - ✅ Broadcast WebSocket → mise à jour Dashboard temps réel
Récupérer l'état de la lumière
Réponse :
{
"light": true,
"status": "OK"
}Inverser l'état de la lumière
Réponse :
{
"message": "Light toggled",
"light": false,
"status": "OK"
}Récupérer l'état du chauffage
Réponse :
{
"heat": true,
"status": "OK"
}Inverser l'état du chauffage
Réponse :
{
"message": "Heat toggled",
"heat": false,
"status": "OK"
}Récupérer l'état de la porte
Réponse :
{
"door": false,
"status": "OK"
}Inverser l'état de la porte
Réponse :
{
"message": "Door toggled",
"door": true,
"status": "OK"
}Récupérer l'historique des événements
Query Parameters (optionnel) :
?type=TEMPERATURE&limit=50
Réponse :
{
"history": [
{
"id": 1,
"type": "TEMPERATURE",
"value": "22.5",
"createdAt": "2025-12-21T14:30:00.000Z"
},
{
"id": 2,
"type": "LIGHT",
"value": "true",
"createdAt": "2025-12-21T14:25:00.000Z"
}
],
"count": 2
}Connexion temps réel pour mises à jour automatiques
URL :
ws://192.168.4.2:3000/ws
Événements (voir section WebSocket ci-dessous)
Le système intègre un moteur de règles événementielles qui s'exécute automatiquement à chaque changement d'état.
// src/rules/engine.ts
export const initRuleEngine = () => {
eventBus.on(EVENTS.STATE_CHANGE, async () => {
const currentState = await HomeStateService.get();
for (const rule of RULES) {
if (rule.condition(currentState)) {
await rule.action();
}
}
});
};Condition :
temp < 19°C && heat === false && door === falseAction :
🔥 Active le chauffage automatiquementExemple :
Il fait 18°C, la porte est fermée et le chauffage est éteint.
→ Le système active automatiquement le chauffage.
Condition :
temp > 23°C && heat === trueAction :
❄️ Désactive le chauffage (confort atteint)Exemple :
La température atteint 24°C grâce au chauffage.
→ Le système coupe automatiquement le chauffage pour éviter la surchauffe.
Condition :
door === true && light === falseAction :
💡 Allume la lumière (bienvenue)Exemple :
La porte s'ouvre et la lumière est éteinte.
→ Le système allume automatiquement la lumière pour accueillir.
Condition :
door === true && heat === trueAction :
💸 Coupe le chauffage (porte ouverte = gaspillage)Exemple :
La porte est ouverte alors que le chauffage fonctionne.
→ Le système coupe immédiatement le chauffage pour économiser l'énergie.
export const RULES: Rule[] = [
// ... règles existantes
{
id: "NIGHT_MODE",
description: "Turn off lights at 23:00",
condition: (state) => {
const hour = new Date().getHours();
return hour === 23 && state.light === true;
},
action: async () => {
await HomeStateService.setLight(false);
}
}
];Le serveur utilise un système de publication/souscription basé sur EventEmitter pour la communication temps réel.
// src/utils/eventBus.ts
export const eventBus = new EventEmitter();
export const EVENTS = {
STATE_CHANGE: "STATE_CHANGE",
NEW_CONNECTION: "NEW_CONNECTION"
};Endpoint : ws://192.168.4.2:3000/ws
Envoyé immédiatement après la connexion.
{
"type": "INIT",
"data": {
"id": 1,
"temperature": "22.5",
"light": true,
"door": false,
"heat": true,
"updatedAt": "2025-12-21T14:30:00.000Z",
"createdAt": "2025-12-21T10:00:00.000Z"
}
}Envoyé à chaque modification (POST /temp, /toggle/*).
Exemple Température :
{
"type": "UPDATE",
"data": {
"type": "TEMPERATURE",
"value": "23.5"
}
}Exemple Toggle :
{
"type": "UPDATE",
"data": {
"type": "LIGHT",
"value": "false"
}
}IP : 192.168.4.4
1️⃣ Mesure Température Automatique
- Envoi automatique toutes les 3 secondes
- Activable/désactivable via bouton toggle
- Mode manuel toujours disponible (bouton "Envoyer")
2️⃣ Pages de Navigation
- Page 1 : Authentification (affichage statut connexion)
- Page 2 : Température (lecture + envoi manuel)
- Page 3 : Lumière (toggle)
- Page 4 : Chauffage (toggle)
- Page 5 : Porte (toggle)
IP : 192.168.4.5
1️⃣ Affichage État Complet
- Vue unique avec tous les paramètres :
- 🌡️ Température actuelle
- 💡 État lumière
- 🚪 État porte
- 🔥 État chauffage
2️⃣ Rafraîchissement Automatique
- Intervalle : 5 secondes
- Activable/désactivable via bouton
- Requêtes parallèles pour performance
3️⃣ Indicateurs LED
- LED 1 : Lumière (ON = allumée, OFF = éteinte)
- LED 2 : Chauffage (ON = allumé, OFF = éteint)
- LED 3 : Porte (ON = ouverte, OFF = fermée)
✨ Design Responsive - Mobile, tablette, desktop
🔄 Temps Réel - Mise à jour instantanée via WebSocket
🎨 UX Intuitive - Dégradé de couleur selon température
📊 Historique Visuel - Graphiques de tendances
📸 Surveillance ESP - Flux quasi-temps réel des écrans ESP
🎛️ Contrôles Interactifs - Toggle depuis le dashboard
# Exécuter tous les tests
bun test
# Tests avec couverture de code
bun test:coverage
# Résultats
✓ routes/auth.test.ts (3 tests)
✓ routes/check.test.ts (4 tests)
✓ routes/features.test.ts (12 tests)
✓ routes/public.test.ts (2 tests)
✓ routes/ws.test.ts (5 tests)
✓ rules/engine.test.ts (8 tests)
✓ utils/crypto.test.ts (4 tests)
Total: 38 tests passed
Coverage: 95.2% statements# Linting & Formatting (Biome)
bun run lint
bun run format
bun run check # lint + format ensemble
# Détection code mort
bun run knip
# Exemple sortie Knip
✓ No unused dependencies
✓ No unused files
✓ All exports are used# lefthook.yml
pre-commit:
commands:
lint:
run: bun run check
test:
run: bun testBunServer/
├── 📄 index.ts # Point d'entrée principal
├── 📄 package.json # Dépendances & scripts
├── 📄 tsconfig.json # Configuration TypeScript
├── 📄 biome.json # Linter/Formatter config
├── 📄 knip.json # Dead code detector
├── 📄 lefthook.yml # Git hooks
├── 📄 Dockerfile # Image Docker multi-stage
├── 📄 compose.yaml # Orchestration services
├── 📄 DOCUMENTATION.md # Documentation supplémentaire
├── 📄 INFRASTRUCTURE_NOTES.txt # Notes architecture (FR)
│
├── 📁 prisma/
│ ├── schema.prisma # Schéma DB Prisma
│ ├── db.ts # Client Prisma configuré
│ ├── seed.ts # Données initiales
│ ├── prisma.config.ts # Configuration Prisma
│ ├── migrations/ # Historique migrations SQL
│ └── generated/ # Client Prisma généré (auto)
│
├── 📁 src/
│ ├── enums.ts # Énumérations globales
│ │
│ ├── 📁 middleware/
│ │ └── auth.ts # Middleware authentification
│ │
│ ├── 📁 routes/
│ │ ├── index.ts # Routeur principal
│ │ ├── auth/index.ts # POST /auth
│ │ ├── check/index.ts # GET /check
│ │ ├── history/index.ts # GET /history
│ │ ├── status/index.ts # GET /status
│ │ ├── temp/index.ts # GET/POST /temp
│ │ ├── ws/index.ts # WS /ws
│ │ └── toggle/
│ │ ├── index.ts # Routeur toggle
│ │ ├── light.ts # GET/POST /toggle/light
│ │ ├── heat.ts # GET/POST /toggle/heat
│ │ └── door.ts # GET/POST /toggle/door
│ │
│ ├── 📁 rules/
│ │ ├── definitions.ts # Définition des règles
│ │ └── engine.ts # Moteur d'exécution
│ │
│ ├── 📁 services/
│ │ └── homeState.ts # Service métier HomeState
│ │
│ └── 📁 utils/
│ ├── crypto.ts # AES-256 encrypt/decrypt
│ └── eventBus.ts # EventEmitter pub/sub
│
└── 📁 tests/
├── routes/ # Tests endpoints API
│ ├── auth.test.ts
│ ├── check.test.ts
│ ├── features.test.ts
│ ├── public.test.ts
│ └── ws.test.ts
├── rules/
│ └── engine.test.ts # Tests moteur de règles
└── utils/
└── crypto.test.ts # Tests cryptographie
# Démarrage serveur
bun run start # Production
bun run dev # Développement (avec watch)
# Base de données
bun run prisma:generate # Générer client Prisma
bun run prisma:migrate # Créer migration
bun run prisma:push # Push schéma (sans migration)
bun run prisma:studio # Interface GUI DB
bun run seed # Initialiser données
# Qualité de code
bun run lint # Vérifier code
bun run format # Formater code
bun run check # Lint + Format
bun run knip # Détecter code mort
# Tests
bun test # Tests unitaires
bun test:coverage # Avec couverture- Créer une branche
git checkout -b feature/new-rule- Développer & Tester
bun run check # Vérifier qualité
bun test # Lancer tests- Commit (hooks automatiques)
git add .
git commit -m "feat: add night mode rule"
# ✓ Lefthook exécute lint + tests automatiquement- Push & PR
git push origin feature/new-ruleCréer .env à la racine :
# Database
DATABASE_URL="postgresql://root:root@localhost:5432/myhouse"
# Server
PORT_BUN_SERVER=3000
PORT_WEB_SERVER=8080
NODE_ENV=development
# Crypto (optionnel)
ENCRYPTION_KEY=your-32-character-secret-key
ENCRYPTION_SALT="MyFixedSalt"- 🚀 Bun - Runtime JavaScript ultra-rapide
- ⚡ Elysia - Framework web pour Bun
- 🗄️ Prisma - ORM moderne type-safe
- 🧪 Biome - Toolchain tout-en-un
- 🐳 Docker - Containerisation
Projet développé dans le cadre de l'évaluation du module de domotique.
Pour toute question ou problème :
- Consulter la documentation Swagger
- Vérifier les logs Docker
- Tester avec Prisma Studio
⚡ Construit avec Bun - Le runtime JavaScript le plus rapide au monde
Made with ❤️ by Antoine, Ilian, François et Hugo