Contribuer à Sowel¶
Ce guide couvre tout ce qu'un contributeur doit savoir pour mettre en place l'environnement de développement, suivre les conventions du projet et soumettre des changements.
Mise en place du dev¶
Prérequis¶
- Node.js 20+
- npm (livré avec Node.js)
- Docker + docker-compose (pour InfluxDB)
- Git
Backend¶
git clone <repo-url>
cd sowel
npm install
npm run dev # Development with hot reload (ts-node + nodemon)
Frontend¶
Docker (InfluxDB)¶
InfluxDB est obligatoire : Sowel se connecte au démarrage et auto-crée les buckets, les tâches de downsampling et les tâches d'agrégation énergétique. Aucune configuration manuelle n'est nécessaire.
Build¶
# Backend: TypeScript compilation
npm run build
# Frontend: production build
cd ui && npm run build
# Start production
npm start
Tests¶
Les tests utilisent Vitest avec des bases SQLite en mémoire et de faux timers.
Workflow Git¶
Stratégie de branches¶
- Feature branches obligatoires : tout développement non trivial (nouvelle fonctionnalité, refactoring, changements multi-fichiers) doit se faire sur une branche dédiée, pas directement sur
main. - Utilisez des noms de branche descriptifs :
feat/gate-abstraction,fix/rate-limit,refactor/influxdb-mandatory-core. - Les petits correctifs isolés (typo, changement de config sur une ligne) peuvent aller directement sur
main.
Pull Requests¶
- Le merge d'une PR exige une approbation explicite de l'utilisateur : ne mergez jamais une pull request sans validation explicite du mainteneur.
- Créez la PR, présentez-la, et attendez l'approbation avant de merger.
- Gardez les PRs ciblées : une fonctionnalité ou un correctif par PR.
Messages de commit¶
Suivez le style conventional commits :
feat(devices): add auto-discovery for Netatmo HC
fix(energy): correct HP/HC timestamp offset
refactor(core): make InfluxDB mandatory
docs(api): update endpoint documentation
Conventions de code¶
TypeScript¶
- Le strict mode est activé sur tout le projet.
- Tous les types sont définis dans
src/shared/types.tset partagés entre les modules backend. - Utilisez les unions discriminées TypeScript pour l'Event Bus typé (type
EngineEvent). - Exécutez toujours
npx tsc --noEmitavant de committer pour attraper les erreurs de type.
IDs et données¶
- UUID v4 pour tous les IDs d'entité :
crypto.randomUUID(). - Toutes les dates en format ISO 8601.
- Toutes les interfaces vivent dans
src/shared/types.ts.
Base de données¶
- SQLite via l'API synchrone de
better-sqlite3, volontairement synchrone, très rapide. - Mode WAL :
PRAGMA journal_mode=WAL. - Migrations dans le répertoire
migrations/, exécutées automatiquement au démarrage. - Utilisez les transactions pour les opérations en lot.
- Les fichiers de migration suivent le pattern de nommage
NNN_description.sql(par ex.033_plugins.sql).
Event Bus¶
EventEmittertypé avec union discriminée TypeScript (typeEngineEvent).- Tous les handlers doivent être non bloquants et ne jamais lever d'exception.
- Wrappez toute la logique de handler dans try/catch avec logging.
Intégrations¶
- Chaque source de device implémente l'interface
IntegrationPlugin. - Les plugins s'enregistrent dans
IntegrationRegistryqui gère le cycle de vie (start/stop/reconnect). - Les intégrations basées sur MQTT utilisent
mqtt.jsavecconnectAsyncpour async/await. - Les intégrations cloud utilisent du polling avec des intervalles configurables.
- Tous les handlers de message/événement ne doivent jamais lever d'exception, wrappez en try/catch avec logging.
- Les réglages stockés en SQLite dans la table
settingssousintegration.<id>.<key>, configurables depuis l'UI.
Authentification¶
- bcrypt (cost 12) pour les mots de passe,
jsonwebtoken(HS256) pour les JWT. - Tokens d'API : préfixe
swl_, hash SHA-256 stocké, généré viacrypto.randomBytes(32). - Middleware d'auth : essaie d'abord le décodage JWT, puis le lookup de token d'API.
- Rôles :
admin>standard>viewer(permissions hiérarchiques).
Moteur d'expressions¶
- Parser d'expressions sûr (PAS
eval), à envisagerexpr-evalou un parseur maison. - Références :
binding.<alias>,equipment.<id>.<key>,zone.<zoneId>.<key>. - Opérateurs :
OR,AND,NOT,AVG,MIN,MAX,SUM,IF,THRESHOLD.
Frontend¶
- Stores Zustand mis à jour par les événements WebSocket.
- WebSocket auto-reconnect avec récupération d'état (incrémental ou complet).
- Tailwind CSS utility classes uniquement, pas de fichiers CSS personnalisés.
- Design responsive mobile-first (breakpoints : 640 px, 1024 px).
- Dark mode via la stratégie
classde Tailwind. - Icônes : Lucide React, stroke 1.5px.
Règles de logging¶
Logs JSON structurés via pino (par défaut Fastify) avec multistream : ring buffer (UI), pino-pretty (dev), JSON stdout + fichiers pino-roll (prod).
Niveaux de log¶
| Niveau | Usage | Visible en prod | Exemples |
|---|---|---|---|
| fatal | Le processus est sur le point de crasher, irrécupérable | Oui | Exception non capturée, corruption de base |
| error | Opération échouée, le moteur continue. Demande attention | Oui | Poll d'intégration échoué, erreur de dispatch d'ordre, erreur d'exécution de recette |
| warn | Situation inattendue, gérée proprement | Oui | MQTT en reconnexion, device offline, retry de refresh de token, nettoyage de device obsolète |
| info | Événements métier significatifs, un par opération | Oui | Démarrage/arrêt moteur, device découvert/supprimé, CRUD d'équipement, mode activé |
| debug | Détail opérationnel pour le dépannage | Non (dev/UI seul) | Évaluation de binding, étapes d'agrégation, migration appliquée, config chargée |
| trace | Données de chemin chaud à haut volume, debugging profond seul | Non (dev/UI seul) | Chaque émission d'event bus, chaque message MQTT, chaque mise à jour de point de donnée |
Règles d'attribution de niveau¶
- info = tableau de bord admin : un opérateur qui lit les logs info doit comprendre ce qui s'est passé sans se noyer. Un log par opération métier, pas par item traité.
- debug = session développeur : assez détaillé pour tracer un problème spécifique. Un humain peut lire ces logs sur un module pendant une session de debug.
- trace = mode replay : permet de reproduire les transitions d'état exactes. Volume élevé, jamais activé en production.
- error inclut toujours
{ err }: passez l'objet Error en contexte structuré, par ex.logger.error({ err }, "Poll failed"). - warn = auto-réparation : le système a géré, mais des warnings répétés signalent une dégradation.
- N'utilisez jamais
console.log/error/warn: utilisez toujours le logger pino structuré. Les appels console contournent le ring buffer, la rotation des fichiers et la redaction.
Que mettre où (par domaine)¶
| Domaine | info | debug | trace |
|---|---|---|---|
| MQTT | Connecté, déconnecté, en reconnexion | Souscrit à un topic, résultat de publication | Chaque message reçu |
| Devices | Découvert, supprimé, statut changé | Donnée auto-créée, catégorie inférée | Chaque mise à jour de donnée |
| Equipments | CRUD, ordre dispatché | Évaluation de binding, résultat de computed data | Chaque réévaluation de binding |
| Zones | CRUD, résumé d'agrégation | Champs d'agrégation calculés individuellement | Chaque déclencheur d'agrégation |
| Modes | Activé, désactivé, CRUD | Chaque action d'impact exécutée | -- |
| Recipes | Instance créée/supprimée, activée/désactivée | Étapes d'exécution, évaluation de trigger | -- |
| Integrations | Démarré, stoppé, connecté, poll terminé | Détails de cycle de poll, résultats d'appel API | Réponses API brutes |
| Auth | Login utilisateur, token créé, mot de passe changé | Validation JWT, décisions du middleware | -- |
| API | Serveur en écoute, route enregistrée | Détails de traitement de requête | Chaque requête/réponse |
| WebSocket | Client connecté/déconnecté | Souscription topic, message envoyé | Chaque frame |
| Event Bus | -- | -- | Chaque événement émis |
| Database | DB ouverte, migration appliquée | Exécution de requête, changements de schéma | -- |
Conventions du logger¶
- Nom de propriété : utilisez
this.loggerdans les classes,loggerdans les fonctions standalone. Jamaisthis.log,log, ouconsole. - Child loggers : chaque module crée un child logger avec
{ module: "module-name" }pour le filtrage. - Contexte structuré : passez les données comme premier argument objet, le message comme second :
logger.info({ deviceId, status }, "Device status changed"). - Données sensibles : auto-redactées par la config pino (mots de passe, tokens, secrets, clés d'API). Ne loggez jamais des credentials manuellement.
- Pas d'interpolation de chaîne dans les messages : utilisez
logger.info({ count }, "Devices discovered")plutôt quelogger.info(`Discovered ${count} devices`).
Référence du design system¶
Quand vous ajoutez ou modifiez des composants UI, suivez ces design tokens :
| Propriété | Valeur |
|---|---|
| Police body | Inter |
| Police mono | JetBrains Mono (valeurs, logs) |
| Couleur primaire | #1A4F6E (ocean blue), hover : #13405A, light : #E6F0F6 |
| Couleur accent | #D4963F (amber), hover : #BB8232 |
| Espacement de base | 4px |
| Border radius | 6px (boutons), 10px (cartes), 14px (modales) |
| Taille de police body | 14px (tableau de bord dense) |
| Valeurs de données | 28px (lisibles d'un coup d'œil) |
| Icônes | Lucide React, stroke 1.5px |
Variables d'environnement¶
Tous les réglages sont optionnels avec des valeurs par défaut raisonnables, Sowel tourne sans configuration. Surchargez via .env au besoin :
| Variable | Défaut | Notes |
|---|---|---|
SQLITE_PATH |
./data/sowel.db |
Chemin de la base SQLite |
API_PORT |
3000 |
Port du serveur HTTP |
API_HOST |
0.0.0.0 |
Adresse de bind |
JWT_SECRET |
auto-généré | Persisté dans data/.jwt-secret au premier lancement |
JWT_ACCESS_TTL |
900 |
TTL de l'access token en secondes (15 min) |
JWT_REFRESH_TTL |
2592000 |
TTL du refresh token en secondes (30 jours) |
LOG_LEVEL |
info |
Niveau de log pino |
CORS_ORIGINS |
* |
Origines autorisées séparées par des virgules |
INFLUX_URL |
http://localhost:8086 |
URL InfluxDB 2.x |
INFLUX_TOKEN |
auto-généré | Persisté dans data/.influx-token au premier lancement |
INFLUX_ORG |
sowel |
Organisation InfluxDB |
INFLUX_BUCKET |
sowel |
Bucket principal InfluxDB |
Les réglages d'intégration (MQTT, identifiants cloud, intervalles de polling) sont configurés depuis l'UI (Administration > Intégrations), pas depuis .env.
Structure du projet¶
Pour un schéma complet de la structure du projet et des détails d'architecture, voir Vue d'ensemble de l'architecture.
Roadmap de développement¶
V0.1 Devices -> V0.2 Zones -> V0.3 Equipments+Bindings -> V0.4 UI Home -> V0.5 Sensors -> V0.6 Zone Aggregation -> V0.7 Shutters -> V0.8 Recipes -> V0.9 Modes+Calendar -> V0.10 Integration Plugins (Z2M, Panasonic CC, MCZ Maestro, Netatmo HC) -> V0.11 Logging -> V0.12 Computed Data -> V0.13 History (InfluxDB) -> V1.0+ AI Assistant
Checklist avant soumission¶
- [ ]
npx tsc --noEmitpasse (backend) - [ ]
cd ui && npx tsc --noEmitpasse (frontend, si modifié) - [ ]
npm testpasse - [ ] Aucun appel
console.log, utilisez le logger pino - [ ] Logging structuré avec les bons niveaux (voir règles ci-dessus)
- [ ] Migrations ajoutées si changement de schéma
- [ ] Branche feature avec un nom descriptif
- [ ] La description de PR explique le "pourquoi"